diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt index 497fcb2..24f06d4 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/sync/RoomPersistence.kt @@ -75,7 +75,7 @@ internal class RoomPersistence( } } - override suspend fun observeUnread(): Flow>> { + override fun observeUnread(): Flow>> { return database.roomEventQueries.selectAllUnread() .asFlow() .mapToList() @@ -107,7 +107,7 @@ internal class RoomPersistence( } } - override suspend fun observeEvent(eventId: EventId): Flow { + override fun observeEvent(eventId: EventId): Flow { return database.roomEventQueries.selectEvent(event_id = eventId.value) .asFlow() .mapToOneNotNull() diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt index 69b6538..a51a200 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt @@ -1,5 +1,6 @@ package app.dapk.st.directory +import android.util.Log import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomMember @@ -17,7 +18,11 @@ value class UnreadCount(val value: Int) typealias DirectoryState = List -data class RoomFoo(val overview: RoomOverview, val unreadCount: UnreadCount, val typing: Typing?) +data class RoomFoo( + val overview: RoomOverview, + val unreadCount: UnreadCount, + val typing: Typing? +) class DirectoryUseCase( private val syncService: SyncService, @@ -35,6 +40,7 @@ class DirectoryUseCase( roomStore.observeUnreadCountById(), syncService.events() ) { userId, overviewState, localEchos, unread, events -> + Log.e("!!!", "got states") overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview -> RoomFoo( overview = roomOverview, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt new file mode 100644 index 0000000..80e49da --- /dev/null +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt @@ -0,0 +1,24 @@ +package app.dapk.st.notifications + +import android.app.NotificationChannel +import android.app.NotificationManager + +private const val channelId = "message" + +class NotificationChannels( + private val notificationManager: NotificationManager +) { + + fun initChannels() { + if (notificationManager.getNotificationChannel(channelId) == null) { + notificationManager.createNotificationChannel( + NotificationChannel( + channelId, + "messages", + NotificationManager.IMPORTANCE_HIGH, + ) + ) + } + } + +} \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt new file mode 100644 index 0000000..de35316 --- /dev/null +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt @@ -0,0 +1,113 @@ +package app.dapk.st.notifications + +import android.app.Notification +import android.app.PendingIntent +import android.app.Person +import android.content.Context +import app.dapk.st.imageloader.IconLoader +import app.dapk.st.matrix.sync.RoomEvent +import app.dapk.st.matrix.sync.RoomOverview +import app.dapk.st.messenger.MessengerActivity + +private const val GROUP_ID = "st" +private const val channelId = "message" + +class NotificationFactory( + private val iconLoader: IconLoader, + private val context: Context, +) { + + suspend fun createNotifications(events: Map>): Notifications { + val notifications = events.map { (roomOverview, events) -> + val messageEvents = events.filterIsInstance() + when (messageEvents.isEmpty()) { + true -> NotificationDelegate.DismissRoom(roomOverview.roomId) + false -> createNotification(messageEvents, roomOverview) + } + } + + val summaryNotification = if (notifications.filterIsInstance().size > 1) { + createSummary(notifications) + } else { + null + } + return Notifications(summaryNotification, notifications) + } + + private fun createSummary(notifications: List): Notification { + val summaryInboxStyle = Notification.InboxStyle().also { style -> + notifications.forEach { + when (it) { + is NotificationDelegate.DismissRoom -> { + // do nothing + } + is NotificationDelegate.Room -> style.addLine(it.summary) + } + } + } + + return Notification.Builder(context, channelId) + .setStyle(summaryInboxStyle) + .setSmallIcon(R.drawable.ic_notification_small_icon) + .setCategory(Notification.CATEGORY_MESSAGE) + .setGroupSummary(true) + .setGroup(GROUP_ID) + .build() + } + + private suspend fun createNotification(events: List, roomOverview: RoomOverview): NotificationDelegate { + val messageStyle = Notification.MessagingStyle( + Person.Builder() + .setName("me") + .setKey(roomOverview.roomId.value) + .build() + ) + + messageStyle.conversationTitle = roomOverview.roomName.takeIf { roomOverview.isGroup } + messageStyle.isGroupConversation = roomOverview.isGroup + + events.sortedBy { it.utcTimestamp }.forEach { message -> + val sender = Person.Builder() + .setName(message.author.displayName ?: message.author.id.value) + .setIcon(message.author.avatarUrl?.let { iconLoader.load(it.value) }) + .setKey(message.author.id.value) + .build() + messageStyle.addMessage( + Notification.MessagingStyle.Message( + message.content, + message.utcTimestamp, + sender, + ) + ) + } + + val openRoomIntent = PendingIntent.getActivity( + context, + 55, + MessengerActivity.newInstance(context, roomOverview.roomId), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationDelegate.Room( + Notification.Builder(context, channelId) + .setWhen(messageStyle.messages.last().timestamp) + .setShowWhen(true) + .setGroup(GROUP_ID) + .setOnlyAlertOnce(roomOverview.isGroup) + .setContentIntent(openRoomIntent) + .setStyle(messageStyle) + .setCategory(Notification.CATEGORY_MESSAGE) + .setShortcutId(roomOverview.roomId.value) + .setSmallIcon(R.drawable.ic_notification_small_icon) + .setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) }) + .setAutoCancel(true) + .build(), + roomId = roomOverview.roomId, + summary = messageStyle.messages.last().text.toString() + ) + + } + +} + +data class Notifications(val summaryNotification: Notification?, val delegates: List) \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt new file mode 100644 index 0000000..f9fb12f --- /dev/null +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt @@ -0,0 +1,43 @@ +package app.dapk.st.notifications + +import android.app.Notification +import android.app.NotificationManager +import app.dapk.st.core.extensions.ifNull +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.sync.RoomEvent +import app.dapk.st.matrix.sync.RoomOverview + +private const val SUMMARY_NOTIFICATION_ID = 101 +private const val MESSAGE_NOTIFICATION_ID = 100 + +class NotificationRenderer( + private val notificationManager: NotificationManager, + private val notificationFactory: NotificationFactory, +) { + + suspend fun render(result: Map>, removedRooms: Set) { + removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) } + val notifications = notificationFactory.createNotifications(result) + + notifications.summaryNotification.ifNull { + notificationManager.cancel(SUMMARY_NOTIFICATION_ID) + } + + notifications.delegates.forEach { + when (it) { + is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID) + is NotificationDelegate.Room -> notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification) + } + } + + notifications.summaryNotification?.let { + notificationManager.notify(SUMMARY_NOTIFICATION_ID, it) + } + } + +} + +sealed interface NotificationDelegate { + data class Room(val notification: Notification, val roomId: RoomId, val summary: String) : NotificationDelegate + data class DismissRoom(val roomId: RoomId) : NotificationDelegate +} \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt index d71de40..a0561c6 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt @@ -23,13 +23,12 @@ class NotificationsModule( fun pushUseCase() = pushService fun syncService() = syncService fun credentialProvider() = credentialsStore - fun roomStore() = roomStore - fun iconLoader() = iconLoader fun firebasePushTokenUseCase() = firebasePushTokenUseCase fun notificationsUseCase() = NotificationsUseCase( roomStore, - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, - iconLoader, - context, + NotificationRenderer(notificationManager(), NotificationFactory(iconLoader, context)), + NotificationChannels(notificationManager()), ) + + private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt index 82de43f..82b5219 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsUseCase.kt @@ -1,49 +1,26 @@ package app.dapk.st.notifications -import android.app.* -import android.app.Notification.InboxStyle -import android.content.Context import app.dapk.st.core.AppLogTag.NOTIFICATION import app.dapk.st.core.log -import app.dapk.st.imageloader.IconLoader import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.matrix.sync.RoomStore -import app.dapk.st.messenger.MessengerActivity import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.onEach -private const val SUMMARY_NOTIFICATION_ID = 101 -private const val MESSAGE_NOTIFICATION_ID = 100 -private const val GROUP_ID = "st" - class NotificationsUseCase( private val roomStore: RoomStore, - private val notificationManager: NotificationManager, - private val iconLoader: IconLoader, - private val context: Context, + private val notificationRenderer: NotificationRenderer, + notificationChannels: NotificationChannels, ) { private val inferredCurrentNotifications = mutableSetOf() - private val channelId = "message" init { - if (notificationManager.getNotificationChannel(channelId) == null) { - notificationManager.createNotificationChannel( - NotificationChannel( - channelId, - "messages", - NotificationManager.IMPORTANCE_HIGH, - ) - ) - } + notificationChannels.initChannels() } suspend fun listenForNotificationChanges() { - // TODO handle redactions by removing and edits by not notifying - roomStore.observeUnread() .drop(1) .onEach { result -> @@ -51,125 +28,12 @@ class NotificationsUseCase( val asRooms = result.keys.map { it.roomId }.toSet() val removedRooms = inferredCurrentNotifications - asRooms - removedRooms.forEach { notificationManager.cancel(it.value, MESSAGE_NOTIFICATION_ID) } inferredCurrentNotifications.clear() inferredCurrentNotifications.addAll(asRooms) - val notifications = result.map { (roomOverview, events) -> - val messageEvents = events.filterIsInstance() - when (messageEvents.isEmpty()) { - true -> NotificationDelegate.DismissRoom(roomOverview.roomId) - false -> createNotification(messageEvents, roomOverview) - } - } - - - val summaryNotification = if (notifications.filterIsInstance().size > 1) { - createSummary(notifications) - } else { - null - } - - if (summaryNotification == null) { - notificationManager.cancel(SUMMARY_NOTIFICATION_ID) - } - - notifications.forEach { - when (it) { - is NotificationDelegate.DismissRoom -> notificationManager.cancel(it.roomId.value, MESSAGE_NOTIFICATION_ID) - is NotificationDelegate.Room -> notificationManager.notify(it.roomId.value, MESSAGE_NOTIFICATION_ID, it.notification) - } - } - - if (summaryNotification != null) { - notificationManager.notify(SUMMARY_NOTIFICATION_ID, summaryNotification) - } - + notificationRenderer.render(result, removedRooms) } .collect() } - - private fun createSummary(notifications: List): Notification { - val summaryInboxStyle = InboxStyle().also { style -> - notifications.forEach { - when (it) { - is NotificationDelegate.DismissRoom -> { - // do nothing - } - is NotificationDelegate.Room -> style.addLine(it.summary) - } - } - } - - return Notification.Builder(context, channelId) - .setStyle(summaryInboxStyle) - .setSmallIcon(R.drawable.ic_notification_small_icon) - .setCategory(Notification.CATEGORY_MESSAGE) - .setGroupSummary(true) - .setGroup(GROUP_ID) - .build() - } - - private suspend fun createNotification(events: List, roomOverview: RoomOverview): NotificationDelegate { - val messageStyle = Notification.MessagingStyle( - Person.Builder() - .setName("me") - .setKey(roomOverview.roomId.value) - .build() - ) - - messageStyle.conversationTitle = roomOverview.roomName.takeIf { roomOverview.isGroup } - messageStyle.isGroupConversation = roomOverview.isGroup - - events.sortedBy { it.utcTimestamp }.forEach { message -> - val sender = Person.Builder() - .setName(message.author.displayName ?: message.author.id.value) - .setIcon(message.author.avatarUrl?.let { iconLoader.load(it.value) }) - .setKey(message.author.id.value) - .build() - messageStyle.addMessage( - Notification.MessagingStyle.Message( - message.content, - message.utcTimestamp, - sender, - ) - ) - } - - val openRoomIntent = PendingIntent.getActivity( - context, - 55, - MessengerActivity.newInstance(context, roomOverview.roomId), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - return NotificationDelegate.Room( - Notification.Builder(context, channelId) - .setWhen(messageStyle.messages.last().timestamp) - .setShowWhen(true) - .setGroup(GROUP_ID) - .setOnlyAlertOnce(roomOverview.isGroup) - .setContentIntent(openRoomIntent) - .setStyle(messageStyle) - .setCategory(Notification.CATEGORY_MESSAGE) - .setShortcutId(roomOverview.roomId.value) - .setSmallIcon(R.drawable.ic_notification_small_icon) - .setLargeIcon(roomOverview.roomAvatarUrl?.let { iconLoader.load(it.value) }) - .setAutoCancel(true) - .build(), - roomId = roomOverview.roomId, - summary = messageStyle.messages.last().text.toString() - ) - - } - } - -sealed interface NotificationDelegate { - - data class Room(val notification: Notification, val roomId: RoomId, val summary: String) : NotificationDelegate - data class DismissRoom(val roomId: RoomId) : NotificationDelegate - - -} \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt index e4368b6..97ddf63 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/Store.kt @@ -12,9 +12,9 @@ interface RoomStore { fun latest(roomId: RoomId): Flow suspend fun insertUnread(roomId: RoomId, eventIds: List) suspend fun markRead(roomId: RoomId) - suspend fun observeUnread(): Flow>> + fun observeUnread(): Flow>> fun observeUnreadCountById(): Flow> - suspend fun observeEvent(eventId: EventId): Flow + fun observeEvent(eventId: EventId): Flow suspend fun findEvent(eventId: EventId): RoomEvent? }