From 0ab3bdc2a03a6fed95e925a93e640385775ea50e Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Mon, 15 Nov 2021 16:24:31 -0500 Subject: [PATCH] Dismiss notifications when detail view is opened, show new bubble --- .../io.heckel.ntfy.data.Database/2.json | 12 ++- .../main/java/io/heckel/ntfy/data/Database.kt | 89 ++++++++++++------- .../java/io/heckel/ntfy/data/Repository.kt | 12 ++- .../java/io/heckel/ntfy/msg/ApiService.kt | 12 ++- .../io/heckel/ntfy/msg/FirebaseService.kt | 15 +++- .../io/heckel/ntfy/msg/NotificationService.kt | 18 +++- .../io/heckel/ntfy/msg/SubscriberService.kt | 6 +- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 57 +++++++++++- .../java/io/heckel/ntfy/ui/MainActivity.kt | 9 +- .../java/io/heckel/ntfy/ui/MainAdapter.kt | 13 ++- .../java/io/heckel/ntfy/work/PollWorker.kt | 13 ++- app/src/main/res/drawable/ic_circle.xml | 5 ++ .../main/res/layout/main_fragment_item.xml | 13 +++ 13 files changed, 213 insertions(+), 61 deletions(-) create mode 100644 app/src/main/res/drawable/ic_circle.xml diff --git a/app/schemas/io.heckel.ntfy.data.Database/2.json b/app/schemas/io.heckel.ntfy.data.Database/2.json index 52562af..54752b3 100644 --- a/app/schemas/io.heckel.ntfy.data.Database/2.json +++ b/app/schemas/io.heckel.ntfy.data.Database/2.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 2, - "identityHash": "df0a0eab3fc3056bf12e04a09c084660", + "identityHash": "4b24fe9241d824ae94f32a31e41841c8", "entities": [ { "tableName": "Subscription", @@ -54,7 +54,7 @@ }, { "tableName": "Notification", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `message` TEXT NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -80,6 +80,12 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, { "fieldPath": "deleted", "columnName": "deleted", @@ -100,7 +106,7 @@ "views": [], "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, 'df0a0eab3fc3056bf12e04a09c084660')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4b24fe9241d824ae94f32a31e41841c8')" ] } } \ No newline at end of file diff --git a/app/src/main/java/io/heckel/ntfy/data/Database.kt b/app/src/main/java/io/heckel/ntfy/data/Database.kt index 7e48f6a..5b1e968 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -12,11 +12,12 @@ data class Subscription( @ColumnInfo(name = "baseUrl") val baseUrl: String, @ColumnInfo(name = "topic") val topic: String, @ColumnInfo(name = "instant") val instant: Boolean, - @Ignore val notifications: Int, + @Ignore val totalCount: Int = 0, // Total notifications + @Ignore val newCount: Int = 0, // New notifications @Ignore val lastActive: Long = 0, // Unix timestamp @Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE ) { - constructor(id: Long, baseUrl: String, topic: String, instant: Boolean) : this(id, baseUrl, topic, instant, 0, 0, ConnectionState.NOT_APPLICABLE) + constructor(id: Long, baseUrl: String, topic: String, instant: Boolean) : this(id, baseUrl, topic, instant, 0, 0, 0, ConnectionState.NOT_APPLICABLE) } enum class ConnectionState { @@ -28,7 +29,8 @@ data class SubscriptionWithMetadata( val baseUrl: String, val topic: String, val instant: Boolean, - val notifications: Int, + val totalCount: Int, + val newCount: Int, val lastActive: Long ) @@ -38,7 +40,8 @@ data class Notification( @ColumnInfo(name = "subscriptionId") val subscriptionId: Long, @ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp @ColumnInfo(name = "message") val message: String, - @ColumnInfo(name = "deleted") val deleted: Boolean + @ColumnInfo(name = "notificationId") val notificationId: Int, // Android notification popup ID + @ColumnInfo(name = "deleted") val deleted: Boolean, ) @androidx.room.Database(entities = [Subscription::class, Notification::class], version = 2) @@ -71,7 +74,8 @@ abstract class Database : RoomDatabase() { db.execSQL("ALTER TABLE Subscription_New RENAME TO Subscription") db.execSQL("CREATE UNIQUE INDEX index_Subscription_baseUrl_topic ON Subscription (baseUrl, topic)") - // Add "deleted" column + // Add "notificationId" & "deleted" columns + db.execSQL("ALTER TABLE Notification ADD COLUMN notificationId INTEGER NOT NULL DEFAULT('0')") db.execSQL("ALTER TABLE Notification ADD COLUMN deleted INTEGER NOT NULL DEFAULT('0')") } } @@ -80,40 +84,56 @@ abstract class Database : RoomDatabase() { @Dao interface SubscriptionDao { - @Query( - "SELECT s.id, s.baseUrl, s.topic, s.instant, COUNT(n.id) notifications, IFNULL(MAX(n.timestamp),0) AS lastActive " + - "FROM subscription AS s " + - "LEFT JOIN notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 " + - "GROUP BY s.id " + - "ORDER BY MAX(n.timestamp) DESC" - ) + @Query(""" + SELECT + s.id, s.baseUrl, s.topic, s.instant, + COUNT(n.id) totalCount, + COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, + IFNULL(MAX(n.timestamp),0) AS lastActive + FROM Subscription AS s + LEFT JOIN Notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 + GROUP BY s.id + ORDER BY MAX(n.timestamp) DESC + """) fun listFlow(): Flow> - @Query( - "SELECT s.id, s.baseUrl, s.topic, s.instant, COUNT(n.id) notifications, IFNULL(MAX(n.timestamp),0) AS lastActive " + - "FROM subscription AS s " + - "LEFT JOIN notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 " + - "GROUP BY s.id " + - "ORDER BY MAX(n.timestamp) DESC" - ) + @Query(""" + SELECT + s.id, s.baseUrl, s.topic, s.instant, + COUNT(n.id) totalCount, + COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, + IFNULL(MAX(n.timestamp),0) AS lastActive + FROM Subscription AS s + LEFT JOIN Notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 + GROUP BY s.id + ORDER BY MAX(n.timestamp) DESC + """) fun list(): List - @Query( - "SELECT s.id, s.baseUrl, s.topic, s.instant, COUNT(n.id) notifications, IFNULL(MAX(n.timestamp),0) AS lastActive " + - "FROM subscription AS s " + - "LEFT JOIN notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 " + - "WHERE s.baseUrl = :baseUrl AND s.topic = :topic " + - "GROUP BY s.id " - ) + @Query(""" + SELECT + s.id, s.baseUrl, s.topic, s.instant, + COUNT(n.id) totalCount, + COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, + IFNULL(MAX(n.timestamp),0) AS lastActive + FROM Subscription AS s + LEFT JOIN Notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 + WHERE s.baseUrl = :baseUrl AND s.topic = :topic + GROUP BY s.id + """) fun get(baseUrl: String, topic: String): SubscriptionWithMetadata? - @Query( - "SELECT s.id, s.baseUrl, s.topic, s.instant, COUNT(n.id) notifications, IFNULL(MAX(n.timestamp),0) AS lastActive " + - "FROM subscription AS s " + - "LEFT JOIN notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 " + - "WHERE s.id = :subscriptionId " + - "GROUP BY s.id " - ) + @Query(""" + SELECT + s.id, s.baseUrl, s.topic, s.instant, + COUNT(n.id) totalCount, + COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, + IFNULL(MAX(n.timestamp),0) AS lastActive + FROM Subscription AS s + LEFT JOIN Notification AS n ON s.id=n.subscriptionId AND n.deleted != 1 + WHERE s.id = :subscriptionId + GROUP BY s.id + """) fun get(subscriptionId: Long): SubscriptionWithMetadata? @Insert @@ -140,6 +160,9 @@ interface NotificationDao { @Query("SELECT * FROM notification WHERE id = :notificationId") fun get(notificationId: String): Notification? + @Update + fun update(notification: Notification) + @Query("UPDATE notification SET deleted = 1 WHERE id = :notificationId") fun remove(notificationId: String) diff --git a/app/src/main/java/io/heckel/ntfy/data/Repository.kt b/app/src/main/java/io/heckel/ntfy/data/Repository.kt index ec78fef..47ecdb6 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt @@ -4,10 +4,12 @@ import android.util.Log import androidx.annotation.WorkerThread import androidx.lifecycle.* import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong class Repository(private val subscriptionDao: SubscriptionDao, private val notificationDao: NotificationDao) { private val connectionStates = ConcurrentHashMap() private val connectionStatesLiveData = MutableLiveData(connectionStates) + val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ... init { Log.d(TAG, "Created $this") @@ -87,6 +89,10 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif return false } + fun updateNotification(notification: Notification) { + notificationDao.update(notification) + } + @Suppress("RedundantSuspendModifier") @WorkerThread suspend fun removeNotification(notificationId: String) { @@ -107,8 +113,9 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif baseUrl = s.baseUrl, topic = s.topic, instant = s.instant, + totalCount = s.totalCount, + newCount = s.newCount, lastActive = s.lastActive, - notifications = s.notifications, state = connectionState ) } @@ -123,8 +130,9 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif baseUrl = s.baseUrl, topic = s.topic, instant = s.instant, + totalCount = s.totalCount, + newCount = s.newCount, lastActive = s.lastActive, - notifications = s.notifications, state = getState(s.id) ) } diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt index fec1629..96ab997 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -10,6 +10,7 @@ import okhttp3.* import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException import java.util.concurrent.TimeUnit +import kotlin.random.Random class ApiService { private val gson = Gson() @@ -74,7 +75,14 @@ class ApiService { val line = source.readUtf8Line() ?: throw Exception("Unexpected response for $url: line is null") val message = gson.fromJson(line, Message::class.java) if (message.event == EVENT_MESSAGE) { - val notification = Notification(message.id, subscriptionId, message.time, message.message, false) + val notification = Notification( + id = message.id, + subscriptionId = subscriptionId, + timestamp = message.time, + message = message.message, + notificationId = Random.nextInt(), + deleted = false + ) notify(notification) } } @@ -93,7 +101,7 @@ class ApiService { private fun fromString(subscriptionId: Long, s: String): Notification { val n = gson.fromJson(s, Message::class.java) - return Notification(n.id, subscriptionId, n.time, n.message, false) + return Notification(n.id, subscriptionId, n.time, n.message, notificationId = 0, deleted = false) } private data class Message( diff --git a/app/src/main/java/io/heckel/ntfy/msg/FirebaseService.kt b/app/src/main/java/io/heckel/ntfy/msg/FirebaseService.kt index 0088f3b..6582aeb 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/FirebaseService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/FirebaseService.kt @@ -9,6 +9,7 @@ import io.heckel.ntfy.data.Notification import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlin.random.Random class FirebaseService : FirebaseMessagingService() { private val repository by lazy { (application as Application).repository } @@ -39,13 +40,21 @@ class FirebaseService : FirebaseMessagingService() { // Add notification val subscription = repository.getSubscription(baseUrl, topic) ?: return@launch - val notification = Notification(id = id, subscriptionId = subscription.id, timestamp = timestamp, message = message, deleted = false) + val notification = Notification( + id = id, + subscriptionId = subscription.id, + timestamp = timestamp, + message = message, + notificationId = Random.nextInt(), + deleted = false + ) val added = repository.addNotification(notification) + val detailViewOpen = repository.detailViewSubscriptionId.get() == subscription.id // Send notification (only if it's not already known) - if (added) { + if (added && !detailViewOpen) { Log.d(TAG, "Sending notification for message: from=${remoteMessage.from}, data=${data}") - notifier.send(subscription, message) + notifier.send(subscription, notification) } } } diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt index 4cfea53..efef910 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -11,6 +11,7 @@ import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import io.heckel.ntfy.R +import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.data.topicShortUrl import io.heckel.ntfy.ui.DetailActivity @@ -18,15 +19,16 @@ import io.heckel.ntfy.ui.MainActivity import kotlin.random.Random class NotificationService(val context: Context) { - fun send(subscription: Subscription, message: String) { + fun send(subscription: Subscription, notification: Notification) { val title = topicShortUrl(subscription.baseUrl, subscription.topic) - Log.d(TAG, "Displaying notification $title: $message") + Log.d(TAG, "Displaying notification $title: ${notification.message}") // Create an Intent for the activity you want to start val intent = Intent(context, DetailActivity::class.java) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL, subscription.baseUrl) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC, subscription.topic) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant) val pendingIntent: PendingIntent? = TaskStackBuilder.create(context).run { addNextIntentWithParentStack(intent) // Add the intent, which inflates the back stack getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT) // Get the PendingIntent containing the entire back stack @@ -36,7 +38,7 @@ class NotificationService(val context: Context) { val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification_icon) .setContentTitle(title) - .setContentText(message) + .setContentText(notification.message) .setSound(defaultSoundUri) .setContentIntent(pendingIntent) // Click target for notification .setAutoCancel(true) // Cancel when notification is clicked @@ -47,7 +49,15 @@ class NotificationService(val context: Context) { val channel = NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT) notificationManager.createNotificationChannel(channel) } - notificationManager.notify(Random.nextInt(), notificationBuilder.build()) + notificationManager.notify(notification.notificationId, notificationBuilder.build()) + } + + fun cancel(notification: Notification) { + if (notification.notificationId != 0) { + Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}") + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(notification.notificationId) + } } companion object { diff --git a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt index 09b57e4..e927b2b 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/SubscriberService.kt @@ -205,9 +205,11 @@ class SubscriberService : Service() { Log.d(TAG, "[$url] Received notification: $n") scope.launch(Dispatchers.IO) { val added = repository.addNotification(n) - if (added) { + val detailViewOpen = repository.detailViewSubscriptionId.get() == subscription.id + + if (added && !detailViewOpen) { Log.d(TAG, "[$url] Showing notification: $n") - notifier.send(subscription, n.message) + notifier.send(subscription, n) } } } 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 90de1a2..0ff3624 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -26,11 +26,12 @@ import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.topicShortUrl import io.heckel.ntfy.data.topicUrl import io.heckel.ntfy.msg.ApiService +import io.heckel.ntfy.msg.NotificationService import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.* - -// TODO dismiss notifications when navigating to detail page +import java.util.concurrent.atomic.AtomicLong class DetailActivity : AppCompatActivity(), ActionMode.Callback { private val viewModel by viewModels { @@ -39,6 +40,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback { private val repository by lazy { (application as Application).repository } private val api = ApiService() private var subscriberManager: SubscriberManager? = null // Context-dependent + private var notifier: NotificationService? = null // Context-dependent // Which subscription are we looking at private var subscriptionId: Long = 0L // Set in onCreate() @@ -63,6 +65,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback { // Dependencies that depend on Context subscriberManager = SubscriberManager(this) + notifier = NotificationService(this) // Show 'Back' button supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -105,6 +108,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback { viewModel.list(subscriptionId).observe(this) { it?.let { + // Show list view adapter.submitList(it as MutableList) if (it.isEmpty()) { mainListContainer.visibility = View.GONE @@ -113,13 +117,61 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback { mainListContainer.visibility = View.VISIBLE noEntriesText.visibility = View.GONE } + + // Cancel notifications that still have popups + maybeCancelNotificationPopups(it) } } + // Scroll up when new notification is added + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0) { + Log.d(TAG, "$itemCount item(s) inserted at $positionStart, scrolling to the top") + mainList.scrollToPosition(positionStart) + } + } + }) + // React to changes in fast delivery setting repository.getSubscriptionIdsWithInstantStatusLiveData().observe(this) { subscriberManager?.refreshService(it) } + + // Mark this subscription as "open" so we don't receive notifications for it + Log.d(TAG, "onCreate hook: Marking subscription $subscriptionId as 'open'") + repository.detailViewSubscriptionId.set(subscriptionId) + } + + override fun onResume() { + super.onResume() + + Log.d(TAG, "onResume hook: Marking subscription $subscriptionId as 'open'") + repository.detailViewSubscriptionId.set(subscriptionId) // Mark as "open" so we don't send notifications while this is open + } + + override fun onPause() { + super.onPause() + Log.d(TAG, "onResume hook: Marking subscription $subscriptionId as 'not open'") + repository.detailViewSubscriptionId.set(0) // Mark as closed + } + + override fun onDestroy() { + repository.detailViewSubscriptionId.set(0) // Mark as closed + Log.d(TAG, "onDestroy hook: Marking subscription $subscriptionId as 'not open'") + super.onDestroy() + } + + private fun maybeCancelNotificationPopups(notifications: List) { + val notificationsWithPopups = notifications.filter { notification -> notification.notificationId != 0 } + if (notificationsWithPopups.isNotEmpty()) { + lifecycleScope.launch(Dispatchers.IO) { + notificationsWithPopups.forEach { notification -> + notifier?.cancel(notification) + repository.updateNotification(notification.copy(notificationId = 0)) + } + } + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -421,5 +473,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback { companion object { const val TAG = "NtfyDetailActivity" + const val CANCEL_NOTIFICATION_DELAY_MILLIS = 20_000L } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index 054120e..31fe6d7 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -165,7 +165,8 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback { baseUrl = baseUrl, topic = topic, instant = instant, - notifications = 0, + totalCount = 0, + newCount = 0, lastActive = Date().time/1000 ) viewModel.add(subscription) @@ -221,8 +222,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback { val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic) val newNotifications = repository.onlyNewNotifications(subscription.id, notifications) newNotifications.forEach { notification -> - repository.addNotification(notification) - notifier?.send(subscription, notification.message) + val notificationWithId = notification.copy(notificationId = Random.nextInt()) + repository.addNotification(notificationWithId) + notifier?.send(subscription, notificationWithId) newNotificationsCount++ } } @@ -262,7 +264,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback { val subscriptionId = data?.getLongExtra(EXTRA_SUBSCRIPTION_ID, 0) val subscriptionBaseUrl = data?.getStringExtra(EXTRA_SUBSCRIPTION_BASE_URL) val subscriptionTopic = data?.getStringExtra(EXTRA_SUBSCRIPTION_TOPIC) - val subscriptionInstant = data?.getBooleanExtra(EXTRA_SUBSCRIPTION_INSTANT, false) Log.d(TAG, "Deleting subscription with subscription ID $subscriptionId (topic: $subscriptionTopic)") subscriptionId?.let { id -> viewModel.remove(id) } diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt index bc22b6b..49c258b 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt @@ -50,13 +50,14 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon private val statusView: TextView = itemView.findViewById(R.id.main_item_status) private val dateView: TextView = itemView.findViewById(R.id.main_item_date) private val instantImageView: View = itemView.findViewById(R.id.main_item_instant_image) + private val newItemsView: TextView = itemView.findViewById(R.id.main_item_new) fun bind(subscription: Subscription) { this.subscription = subscription - var statusMessage = if (subscription.notifications == 1) { - context.getString(R.string.main_item_status_text_one, subscription.notifications) + var statusMessage = if (subscription.totalCount == 1) { + context.getString(R.string.main_item_status_text_one, subscription.totalCount) } else { - context.getString(R.string.main_item_status_text_not_one, subscription.notifications) + context.getString(R.string.main_item_status_text_not_one, subscription.totalCount) } if (subscription.instant && subscription.state == ConnectionState.RECONNECTING) { statusMessage += ", " + context.getString(R.string.main_item_status_reconnecting) @@ -76,6 +77,12 @@ class MainAdapter(private val onClick: (Subscription) -> Unit, private val onLon } else { instantImageView.visibility = View.GONE } + if (subscription.newCount > 0) { + newItemsView.visibility = View.VISIBLE + newItemsView.text = if (subscription.newCount <= 99) subscription.newCount.toString() else "99+" + } else { + newItemsView.visibility = View.GONE + } itemView.setOnClickListener { onClick(subscription) } itemView.setOnLongClickListener { onLongClick(subscription); true } if (selected.contains(subscription.id)) { diff --git a/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt b/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt index 0170e15..2edb2f2 100644 --- a/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/work/PollWorker.kt @@ -11,6 +11,7 @@ import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.NotificationService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlin.random.Random class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { // Every time the worker is changed, the periodic work has to be REPLACEd. @@ -27,10 +28,16 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, try { repository.getSubscriptions().forEach{ subscription -> val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic) - val newNotifications = repository.onlyNewNotifications(subscription.id, notifications) + val newNotifications = repository + .onlyNewNotifications(subscription.id, notifications) + .map { it.copy(notificationId = Random.nextInt()) } newNotifications.forEach { notification -> - repository.addNotification(notification) - notifier.send(subscription, notification.message) + val added = repository.addNotification(notification) + val detailViewOpen = repository.detailViewSubscriptionId.get() == subscription.id + + if (added && !detailViewOpen) { + notifier.send(subscription, notification) + } } } Log.d(TAG, "Finished polling for new notifications") diff --git a/app/src/main/res/drawable/ic_circle.xml b/app/src/main/res/drawable/ic_circle.xml new file mode 100644 index 0000000..78a6902 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/layout/main_fragment_item.xml b/app/src/main/res/layout/main_fragment_item.xml index 61cd96f..c254ada 100644 --- a/app/src/main/res/layout/main_fragment_item.xml +++ b/app/src/main/res/layout/main_fragment_item.xml @@ -43,5 +43,18 @@ app:layout_constraintTop_toTopOf="@+id/main_item_instant_image" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="15dp" android:paddingTop="2dp"/> +