Dismiss notifications when detail view is opened, show new bubble
This commit is contained in:
parent
a44e551809
commit
0ab3bdc2a0
13 changed files with 213 additions and 61 deletions
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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<List<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 " +
|
||||
"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<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.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)
|
||||
|
||||
|
|
|
@ -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<Long, ConnectionState>()
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DetailViewModel> {
|
||||
|
@ -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<Notification>)
|
||||
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<Notification>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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")
|
||||
|
|
5
app/src/main/res/drawable/ic_circle.xml
Normal file
5
app/src/main/res/drawable/ic_circle.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval" >
|
||||
<solid android:color="#338574" />
|
||||
</shape>
|
|
@ -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"/>
|
||||
<TextView
|
||||
android:text="99+"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp" android:id="@+id/main_item_new"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_circle"
|
||||
android:gravity="center"
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constraintTop_toBottomOf="@+id/main_item_date"
|
||||
app:layout_constraintEnd_toEndOf="@+id/main_item_date"
|
||||
app:layout_constraintStart_toEndOf="@+id/main_item_instant_image"
|
||||
android:textSize="10sp" android:textStyle="bold"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
Loading…
Reference in a new issue