From 03ed75009156952a59cd45fa457082fde8fb1101 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Mon, 5 Jan 2026 21:37:32 -0500 Subject: [PATCH] Sorta works --- .../io.heckel.ntfy.db.Database/16.json | 375 ++++++++++++++++++ .../java/io/heckel/ntfy/backup/Backuper.kt | 3 + .../main/java/io/heckel/ntfy/db/Database.kt | 44 +- .../main/java/io/heckel/ntfy/db/Repository.kt | 36 +- .../main/java/io/heckel/ntfy/msg/Message.kt | 1 + .../io/heckel/ntfy/msg/NotificationParser.kt | 36 +- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 4 +- .../java/io/heckel/ntfy/ui/DetailAdapter.kt | 10 +- .../java/io/heckel/ntfy/ui/DetailViewModel.kt | 9 +- app/src/main/res/values/strings.xml | 1 + .../heckel/ntfy/firebase/FirebaseService.kt | 2 + 11 files changed, 496 insertions(+), 25 deletions(-) create mode 100644 app/schemas/io.heckel.ntfy.db.Database/16.json diff --git a/app/schemas/io.heckel.ntfy.db.Database/16.json b/app/schemas/io.heckel.ntfy.db.Database/16.json new file mode 100644 index 00000000..eaae9ee3 --- /dev/null +++ b/app/schemas/io.heckel.ntfy.db.Database/16.json @@ -0,0 +1,375 @@ +{ + "formatVersion": 1, + "database": { + "version": 16, + "identityHash": "0ad0c63dd982870549d612ba1dc41608", + "entities": [ + { + "tableName": "Subscription", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, `minPriority` INTEGER NOT NULL, `autoDelete` INTEGER NOT NULL, `insistent` INTEGER NOT NULL, `lastNotificationId` TEXT, `icon` TEXT, `upAppId` TEXT, `upConnectorToken` TEXT, `displayName` TEXT, `dedicatedChannels` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instant", + "columnName": "instant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mutedUntil", + "columnName": "mutedUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minPriority", + "columnName": "minPriority", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoDelete", + "columnName": "autoDelete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insistent", + "columnName": "insistent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT" + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT" + }, + { + "fieldPath": "upAppId", + "columnName": "upAppId", + "affinity": "TEXT" + }, + { + "fieldPath": "upConnectorToken", + "columnName": "upConnectorToken", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "dedicatedChannels", + "columnName": "dedicatedChannels", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Subscription_baseUrl_topic", + "unique": true, + "columnNames": [ + "baseUrl", + "topic" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)" + }, + { + "name": "index_Subscription_upConnectorToken", + "unique": true, + "columnNames": [ + "upConnectorToken" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)" + } + ] + }, + { + "tableName": "Notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `sid` TEXT NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `contentType` TEXT NOT NULL, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `icon_url` TEXT, `icon_contentUri` TEXT, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscriptionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sid", + "columnName": "sid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encoding", + "columnName": "encoding", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "click", + "columnName": "click", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "TEXT" + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon.url", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "icon.contentUri", + "columnName": "icon_contentUri", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.name", + "columnName": "attachment_name", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.type", + "columnName": "attachment_type", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.size", + "columnName": "attachment_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "attachment.expires", + "columnName": "attachment_expires", + "affinity": "INTEGER" + }, + { + "fieldPath": "attachment.url", + "columnName": "attachment_url", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.contentUri", + "columnName": "attachment_contentUri", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.progress", + "columnName": "attachment_progress", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "subscriptionId" + ] + } + }, + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))", + "fields": [ + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "baseUrl" + ] + } + }, + { + "tableName": "CustomHeader", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`baseUrl`, `name`))", + "fields": [ + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "baseUrl", + "name" + ] + } + }, + { + "tableName": "Log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `tag` TEXT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `exception` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "exception", + "columnName": "exception", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0ad0c63dd982870549d612ba1dc41608')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt index a2027b13..aaa00b48 100644 --- a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt +++ b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt @@ -184,6 +184,7 @@ class Backuper(val context: Context) { id = n.id, subscriptionId = n.subscriptionId, timestamp = n.timestamp, + sid = n.sid ?: n.id, title = n.title, message = n.message, contentType = n.contentType, @@ -315,6 +316,7 @@ class Backuper(val context: Context) { id = n.id, subscriptionId = n.subscriptionId, timestamp = n.timestamp, + sid = n.sid, title = n.title, message = n.message, contentType = n.contentType, @@ -391,6 +393,7 @@ data class Notification( val id: String, val subscriptionId: Long, val timestamp: Long, + val sid: String?, // Sequence ID for updating notifications val title: String, val message: String, val contentType: String, // "" or "text/markdown" (empty assumes "text/plain") diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt index b28d6696..fbe8c8de 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -96,7 +96,8 @@ data class SubscriptionWithMetadata( data class Notification( @ColumnInfo(name = "id") val id: String, @ColumnInfo(name = "subscriptionId") val subscriptionId: Long, - @ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp + @ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp in seconds + @ColumnInfo(name = "sid") val sid: String, // Sequence ID for updating notifications @ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "message") val message: String, @ColumnInfo(name = "contentType") val contentType: String, // "" or "text/markdown" (empty assume text/plain) @@ -109,7 +110,32 @@ data class Notification( @ColumnInfo(name = "actions") val actions: List?, @Embedded(prefix = "attachment_") val attachment: Attachment?, @ColumnInfo(name = "deleted") val deleted: Boolean, -) + @Ignore val originalTime: Long = 0 // Original time of the notification sequence (computed, not stored) +) { + // Secondary constructor for Room (without ignored fields) + constructor( + id: String, + subscriptionId: Long, + timestamp: Long, + sid: String, + title: String, + message: String, + contentType: String, + encoding: String, + notificationId: Int, + priority: Int, + tags: String, + click: String, + icon: Icon?, + actions: List?, + attachment: Attachment?, + deleted: Boolean + ) : this( + id, subscriptionId, timestamp, sid, title, message, contentType, encoding, + notificationId, priority, tags, click, icon, actions, attachment, deleted, + originalTime = 0 + ) +} fun Notification.isMarkdown(): Boolean { return contentType == "text/markdown" @@ -208,7 +234,7 @@ data class LogEntry( this(0, timestamp, tag, level, message, exception) } -@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, CustomHeader::class, LogEntry::class], version = 15) +@androidx.room.Database(entities = [Subscription::class, Notification::class, User::class, CustomHeader::class, LogEntry::class], version = 16) @TypeConverters(Converters::class) abstract class Database : RoomDatabase() { abstract fun subscriptionDao(): SubscriptionDao @@ -239,6 +265,7 @@ abstract class Database : RoomDatabase() { .addMigrations(MIGRATION_12_13) .addMigrations(MIGRATION_13_14) .addMigrations(MIGRATION_14_15) + .addMigrations(MIGRATION_15_16) .fallbackToDestructiveMigration(true) .build() this.instance = instance @@ -356,6 +383,14 @@ abstract class Database : RoomDatabase() { db.execSQL("CREATE TABLE CustomHeader (baseUrl TEXT NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY(baseUrl, name))") } } + + private val MIGRATION_15_16 = object : Migration(15, 16) { + override fun migrate(db: SupportSQLiteDatabase) { + // Add sid column, defaulting to the id value + db.execSQL("ALTER TABLE Notification ADD COLUMN sid TEXT NOT NULL DEFAULT ''") + db.execSQL("UPDATE Notification SET sid = id WHERE sid = ''") + } + } } } @@ -474,6 +509,9 @@ interface NotificationDao { @Query("UPDATE notification SET deleted = 1 WHERE id = :notificationId") fun markAsDeleted(notificationId: String) + @Query("UPDATE notification SET deleted = 1 WHERE subscriptionId = :subscriptionId AND sid = :sid") + fun markAsDeletedBySid(subscriptionId: Long, sid: String) + @Query("UPDATE notification SET deleted = 1 WHERE subscriptionId = :subscriptionId") fun markAllAsDeleted(subscriptionId: Long) diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index d042fc56..ee608dda 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -111,7 +111,37 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) } fun getNotificationsLiveData(subscriptionId: Long): LiveData> { - return notificationDao.listFlow(subscriptionId).asLiveData() + return notificationDao.listFlow(subscriptionId).asLiveData().map { notifications -> + groupNotificationsBySid(notifications) + } + } + + /** + * Group notifications by sid (sequence ID) and return only the latest version of each. + * Also tracks the original time (earliest timestamp) for each sequence. + * Notifications are already sorted by timestamp DESC from the DAO query. + */ + private fun groupNotificationsBySid(notifications: List): List { + val latestBySid = mutableMapOf() + val originalTimeBySid = mutableMapOf() + + for (notification in notifications) { + // Track the latest notification for each sid (first one since sorted DESC) + if (!latestBySid.containsKey(notification.sid)) { + latestBySid[notification.sid] = notification + } + // Track the original (earliest) time for each sid + val currentOriginal = originalTimeBySid[notification.sid] + if (currentOriginal == null || notification.timestamp < currentOriginal) { + originalTimeBySid[notification.sid] = notification.timestamp + } + } + + // Return latest notifications with originalTime set, sorted by timestamp descending + return latestBySid.values.map { notification -> + val originalTime = originalTimeBySid[notification.sid] ?: notification.timestamp + notification.copy(originalTime = originalTime) + }.sortedByDescending { it.timestamp }.toMutableList() } fun clearAllNotificationIds(subscriptionId: Long) { @@ -151,6 +181,10 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) notificationDao.markAsDeleted(notificationId) } + fun markAsDeletedBySid(subscriptionId: Long, sid: String) { + notificationDao.markAsDeletedBySid(subscriptionId, sid) + } + fun markAllAsDeleted(subscriptionId: Long) { notificationDao.markAllAsDeleted(subscriptionId) } diff --git a/app/src/main/java/io/heckel/ntfy/msg/Message.kt b/app/src/main/java/io/heckel/ntfy/msg/Message.kt index b34965f2..6b10bdb2 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/Message.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/Message.kt @@ -9,6 +9,7 @@ import com.google.gson.annotations.SerializedName data class Message( val id: String, val time: Long, + val sid: String?, // Sequence ID for updating notifications val event: String, val topic: String, val priority: Int?, diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationParser.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationParser.kt index 5af07418..557c971d 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationParser.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationParser.kt @@ -32,29 +32,29 @@ class NotificationParser { url = message.attachment.url, ) } else null - val actions = if (message.actions != null) { - message.actions.map { a -> - Action( - id = a.id, - action = a.action, - label = a.label, - clear = a.clear, - url = a.url, - method = a.method, - headers = a.headers, - body = a.body, - intent = a.intent, - extras = a.extras, - progress = null, - error = null - ) - } - } else null + val actions = message.actions?.map { a -> + Action( + id = a.id, + action = a.action, + label = a.label, + clear = a.clear, + url = a.url, + method = a.method, + headers = a.headers, + body = a.body, + intent = a.intent, + extras = a.extras, + progress = null, + error = null + ) + } val icon: Icon? = if (message.icon != null && message.icon != "") Icon(url = message.icon) else null + val sid = message.sid ?: message.id // Default to id if sid not provided val notification = Notification( id = message.id, subscriptionId = subscriptionId, timestamp = message.time, + sid = sid, title = message.title ?: "", message = message.message, contentType = message.contentType ?: "", diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index 6c4d369e..4c62cd62 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -332,11 +332,13 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) { val notification = adapter.get(viewHolder.absoluteAdapterPosition) lifecycleScope.launch(Dispatchers.IO) { - repository.markAsDeleted(notification.id) + // Delete all notifications in the sequence (same sid) + repository.markAsDeletedBySid(notification.subscriptionId, notification.sid) } val snackbar = Snackbar.make(mainList, R.string.detail_item_snack_deleted, Snackbar.LENGTH_SHORT) snackbar.setAction(R.string.detail_item_snack_undo) { lifecycleScope.launch(Dispatchers.IO) { + // Note: undo only restores the latest notification, not the entire sequence repository.undeleteNotification(notification.id) } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt index 67f8bd9c..8041a8d4 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -114,7 +114,15 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: val unmatchedTags = unmatchedTags(splitTags(notification.tags)) val message = maybeAppendActionErrors(formatMessage(notification), notification) - dateView.text = formatDateShort(notification.timestamp) + val isModified = notification.originalTime != 0L && notification.originalTime != notification.timestamp + val dateText = if (isModified) { + val originalDate = formatDateShort(notification.originalTime) + val modifiedDate = formatDateShort(notification.timestamp) + context.getString(R.string.detail_item_date_modified, originalDate, modifiedDate) + } else { + formatDateShort(notification.timestamp) + } + dateView.text = dateText if (notification.isMarkdown()) { messageView.autoLinkMask = 0 markwon.setMarkdown(messageView, message.toString()) diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt index 8606a64a..1ec10cf8 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailViewModel.kt @@ -15,7 +15,14 @@ class DetailViewModel(private val repository: Repository) : ViewModel() { } fun markAsDeleted(notificationId: String) = viewModelScope.launch(Dispatchers.IO) { - repository.markAsDeleted(notificationId) + // Look up the notification to get its subscriptionId and sid, then delete the entire sequence + val notification = repository.getNotification(notificationId) + if (notification != null) { + repository.markAsDeletedBySid(notification.subscriptionId, notification.sid) + } else { + // Fallback to deleting by id if notification not found + repository.markAsDeleted(notificationId) + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a3276f7..80538ecd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -145,6 +145,7 @@ Instant delivery off Subscribed to topic %1$s Tags: %1$s + %1$s (modified %2$s) Notification deleted Undo Open file diff --git a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt index 4459df72..a348a063 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -99,6 +99,7 @@ class FirebaseService : FirebaseMessagingService() { val attachmentSize = data["attachment_size"]?.toLongOrNull()?.nullIfZero() val attachmentExpires = data["attachment_expires"]?.toLongOrNull()?.nullIfZero() val attachmentUrl = data["attachment_url"] + val sid = data["sid"] val truncated = (data["truncated"] ?: "") == "1" if (id == null || topic == null || message == null || timestamp == null) { Log.d(TAG, "Discarding unexpected message: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}") @@ -131,6 +132,7 @@ class FirebaseService : FirebaseMessagingService() { id = id, subscriptionId = subscription.id, timestamp = timestamp, + sid = sid ?: id, title = title ?: "", message = message, contentType = contentType ?: "",