Sorta works
This commit is contained in:
parent
5a5d6c9a2f
commit
03ed750091
11 changed files with 496 additions and 25 deletions
375
app/schemas/io.heckel.ntfy.db.Database/16.json
Normal file
375
app/schemas/io.heckel.ntfy.db.Database/16.json
Normal file
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<Action>?,
|
||||
@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<Action>?,
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,37 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
}
|
||||
|
||||
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
|
||||
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<Notification>): List<Notification> {
|
||||
val latestBySid = mutableMapOf<String, Notification>()
|
||||
val originalTimeBySid = mutableMapOf<String, Long>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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 ?: "",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@
|
|||
<string name="detail_instant_delivery_disabled">Instant delivery off</string>
|
||||
<string name="detail_deep_link_subscribed_toast_message">Subscribed to topic %1$s</string>
|
||||
<string name="detail_item_tags">Tags: %1$s</string>
|
||||
<string name="detail_item_date_modified">%1$s (modified %2$s)</string>
|
||||
<string name="detail_item_snack_deleted">Notification deleted</string>
|
||||
<string name="detail_item_snack_undo">Undo</string>
|
||||
<string name="detail_item_menu_open">Open file</string>
|
||||
|
|
|
|||
|
|
@ -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 ?: "",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue