diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt index 4cc279b8bb7..fd2788279ba 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ActiveNotificationsProvider.kt @@ -34,6 +34,7 @@ interface ActiveNotificationsProvider { fun getMembershipNotificationForSession(sessionId: SessionId): List fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? + fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? fun count(sessionId: SessionId): Int } @@ -76,6 +77,11 @@ class DefaultActiveNotificationsProvider( return getNotificationsForSession(sessionId).find { it.id == summaryId } } + override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? { + val fallbackId = NotificationIdProvider.getFallbackNotificationId(sessionId) + return getNotificationsForSession(sessionId).find { it.id == fallbackId } + } + override fun count(sessionId: SessionId): Int { return getNotificationsForSession(sessionId).size } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt index 7b06aae39fd..970f19fd45f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt @@ -12,15 +12,12 @@ import dev.zacsweers.metro.Inject import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent -import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock @Inject class FallbackNotificationFactory( private val clock: SystemClock, - private val stringProvider: StringProvider, ) { fun create( sessionId: SessionId, @@ -36,7 +33,7 @@ class FallbackNotificationFactory( isRedacted = false, isUpdated = false, timestamp = clock.epochMillis(), - description = stringProvider.getString(R.string.notification_fallback_content), + description = "", cause = cause, ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt index 5489fa08946..53315191105 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactory.kt @@ -9,24 +9,18 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification -import android.graphics.Typeface -import android.text.style.StyleSpan -import androidx.core.text.buildSpannedString -import androidx.core.text.inSpans import coil3.ImageLoader import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent -import io.element.android.services.toolbox.api.strings.StringProvider interface NotificationDataFactory { suspend fun toNotifications( @@ -51,16 +45,15 @@ interface NotificationDataFactory { @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - fun toNotifications( + fun toNotification( fallback: List, notificationAccountParams: NotificationAccountParams, - ): List + ): OneShotNotification? fun createSummaryNotification( roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, notificationAccountParams: NotificationAccountParams, ): SummaryNotification } @@ -71,7 +64,6 @@ class DefaultNotificationDataFactory( private val roomGroupMessageCreator: RoomGroupMessageCreator, private val summaryGroupMessageCreator: SummaryGroupMessageCreator, private val activeNotificationsProvider: ActiveNotificationsProvider, - private val stringProvider: StringProvider, ) : NotificationDataFactory { override suspend fun toNotifications( messages: List, @@ -81,10 +73,7 @@ class DefaultNotificationDataFactory( val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() } .groupBy { it.roomId } return messagesToDisplay.flatMap { (roomId, events) -> - val roomName = events.lastOrNull()?.roomName ?: roomId.value - val isDm = events.lastOrNull()?.roomIsDm ?: false val eventsByThreadId = events.groupBy { it.threadId } - eventsByThreadId.map { (threadId, events) -> val notification = roomGroupMessageCreator.createRoomMessage( events = events, @@ -98,7 +87,6 @@ class DefaultNotificationDataFactory( notification = notification, roomId = roomId, threadId = threadId, - summaryLine = createRoomMessagesGroupSummaryLine(events, roomName, isDm), messageCount = events.size, latestTimestamp = events.maxOf { it.timestamp }, shouldBing = events.any { it.noisy } @@ -123,7 +111,6 @@ class DefaultNotificationDataFactory( OneShotNotification( tag = event.roomId.value, notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event), - summaryLine = event.description, isNoisy = event.noisy, timestamp = event.timestamp ) @@ -140,7 +127,6 @@ class DefaultNotificationDataFactory( OneShotNotification( tag = event.eventId.value, notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event), - summaryLine = event.description, isNoisy = event.noisy, timestamp = event.timestamp ) @@ -149,26 +135,31 @@ class DefaultNotificationDataFactory( @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( + override fun toNotification( fallback: List, notificationAccountParams: NotificationAccountParams, - ): List { - return fallback.map { event -> - OneShotNotification( - tag = event.eventId.value, - notification = notificationCreator.createFallbackNotification(notificationAccountParams, event), - summaryLine = event.description.orEmpty(), - isNoisy = false, - timestamp = event.timestamp - ) - } + ): OneShotNotification? { + if (fallback.isEmpty()) return null + val existingNotification = activeNotificationsProvider + .getFallbackNotification(notificationAccountParams.user.userId) + ?.notification + val notification = notificationCreator.createFallbackNotification( + existingNotification, + notificationAccountParams, + fallback, + ) + return OneShotNotification( + tag = "FALLBACK", + notification = notification, + isNoisy = false, + timestamp = fallback.first().timestamp + ) } override fun createSummaryNotification( roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, notificationAccountParams: NotificationAccountParams, ): SummaryNotification { return when { @@ -178,59 +169,17 @@ class DefaultNotificationDataFactory( roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, - fallbackNotifications = fallbackNotifications, notificationAccountParams = notificationAccountParams, ) ) } } - - private fun createRoomMessagesGroupSummaryLine(events: List, roomName: String, roomIsDm: Boolean): CharSequence { - return when (events.size) { - 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDm) - else -> { - stringProvider.getQuantityString( - R.plurals.notification_compat_summary_line_for_room, - events.size, - roomName, - events.size - ) - } - } - } - - private fun createFirstMessageSummaryLine(event: NotifiableMessageEvent, roomName: String, roomIsDm: Boolean): CharSequence { - return if (roomIsDm) { - buildSpannedString { - event.senderDisambiguatedDisplayName?.let { - inSpans(StyleSpan(Typeface.BOLD)) { - append(it) - append(": ") - } - } - append(event.description) - } - } else { - buildSpannedString { - inSpans(StyleSpan(Typeface.BOLD)) { - append(roomName) - append(": ") - event.senderDisambiguatedDisplayName?.let { - append(it) - append(" ") - } - } - append(event.description) - } - } - } } data class RoomNotification( val notification: Notification, val roomId: RoomId, val threadId: ThreadId?, - val summaryLine: CharSequence, val messageCount: Int, val latestTimestamp: Long, val shouldBing: Boolean, @@ -239,7 +188,6 @@ data class RoomNotification( return notification == other.notification && roomId == other.roomId && threadId == other.threadId && - summaryLine.toString() == other.summaryLine.toString() && messageCount == other.messageCount && latestTimestamp == other.latestTimestamp && shouldBing == other.shouldBing @@ -249,7 +197,6 @@ data class RoomNotification( data class OneShotNotification( val notification: Notification, val tag: String, - val summaryLine: CharSequence, val isNoisy: Boolean, val timestamp: Long, ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index 26769f09ba6..c26a495ce8c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -55,12 +55,11 @@ class NotificationRenderer( val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, imageLoader, notificationAccountParams) val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, notificationAccountParams) val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, notificationAccountParams) - val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, notificationAccountParams) + val fallbackNotification = notificationDataFactory.toNotification(groupedEvents.fallbackEvents, notificationAccountParams) val summaryNotification = notificationDataFactory.createSummaryNotification( roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, - fallbackNotifications = fallbackNotifications, notificationAccountParams = notificationAccountParams, ) @@ -107,13 +106,12 @@ class NotificationRenderer( } } - // Show only the first fallback notification - if (fallbackNotifications.isNotEmpty()) { - Timber.tag(loggerTag.value).d("Showing fallback notification") + if (fallbackNotification != null) { + Timber.tag(loggerTag.value).d("Showing or updating fallback notification") notificationDisplayer.showNotification( - tag = "FALLBACK", + tag = fallbackNotification.tag, id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId), - notification = fallbackNotifications.first().notification + notification = fallbackNotification.notification, ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index f7f0c057c67..d283adae714 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -22,7 +22,6 @@ interface SummaryGroupMessageCreator { roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, ): Notification } @@ -45,7 +44,6 @@ class DefaultSummaryGroupMessageCreator( roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, ): Notification { val summaryIsNoisy = roomNotifications.any { it.shouldBing } || invitationNotifications.any { it.isNoisy } || diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index 9533f6b0ac5..9707a353981 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -75,8 +75,9 @@ interface NotificationCreator { ): Notification fun createFallbackNotification( + existingNotification: Notification?, notificationAccountParams: NotificationAccountParams, - fallbackNotifiableEvent: FallbackNotifiableEvent, + fallbackNotifiableEvents: List, ): Notification /** @@ -240,11 +241,13 @@ class DefaultNotificationCreator( .addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent)) .addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent)) // Build the pending intent for when the notification is clicked - .setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent( - sessionId = inviteNotifiableEvent.sessionId, - roomId = inviteNotifiableEvent.roomId, - eventId = null, - )) + .setContentIntent( + pendingIntentFactory.createOpenRoomPendingIntent( + sessionId = inviteNotifiableEvent.sessionId, + roomId = inviteNotifiableEvent.roomId, + eventId = null, + ) + ) .apply { if (inviteNotifiableEvent.noisy) { // Compat @@ -276,12 +279,14 @@ class DefaultNotificationCreator( .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .configureWith(notificationAccountParams) .setAutoCancel(true) - .setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent( - sessionId = simpleNotifiableEvent.sessionId, - roomId = simpleNotifiableEvent.roomId, - eventId = null, - extras = bundleOf(ROOM_OPENED_FROM_NOTIFICATION to true), - )) + .setContentIntent( + pendingIntentFactory.createOpenRoomPendingIntent( + sessionId = simpleNotifiableEvent.sessionId, + roomId = simpleNotifiableEvent.roomId, + eventId = null, + extras = bundleOf(ROOM_OPENED_FROM_NOTIFICATION to true), + ) + ) .apply { if (simpleNotifiableEvent.noisy) { // Compat @@ -295,28 +300,35 @@ class DefaultNotificationCreator( } override fun createFallbackNotification( + existingNotification: Notification?, notificationAccountParams: NotificationAccountParams, - fallbackNotifiableEvent: FallbackNotifiableEvent, + fallbackNotifiableEvents: List, ): Notification { val channelId = notificationChannels.getChannelIdForMessage(false) + val existingCounter = existingNotification + ?.extras + ?.getInt(FALLBACK_COUNTER_EXTRA) + ?: 0 + val counter = existingCounter + fallbackNotifiableEvents.size + val fallbackNotifiableEvent = fallbackNotifiableEvents.first() return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) .setContentTitle(buildMeta.applicationName.annotateForDebug(7)) - .setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8)) + .setContentText( + stringProvider.getQuantityString(R.plurals.notification_fallback_n_content, counter, counter) + .annotateForDebug(8) + ) + .setExtras( + bundleOf( + FALLBACK_COUNTER_EXTRA to counter + ) + ) + .setNumber(counter) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .configureWith(notificationAccountParams) .setAutoCancel(true) .setWhen(fallbackNotifiableEvent.timestamp) - // Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite - // and the user won't have access to the room yet, resulting in an error screen. .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(fallbackNotifiableEvent.sessionId)) - .setDeleteIntent( - pendingIntentFactory.createDismissEventPendingIntent( - fallbackNotifiableEvent.sessionId, - fallbackNotifiableEvent.roomId, - fallbackNotifiableEvent.eventId - ) - ) .setPriority(NotificationCompat.PRIORITY_LOW) .build() } @@ -503,6 +515,7 @@ class DefaultNotificationCreator( companion object { const val MESSAGE_EVENT_ID = "message_event_id" + private const val FALLBACK_COUNTER_EXTRA = "COUNTER" } } diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index aac5c6c72bf..15afb93d3fa 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -15,6 +15,10 @@ "The UnifiedPush notification distributor couldn\'t be registered, so you will not receive notifications anymore. Please check the notifications settings of the app and the status of the push distributor." "You have new messages." + + "You have %d new message." + "You have %d new messages." + "📹 Incoming call" "** Failed to send - please open room" "Join" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt index 573ec374713..04a65073f60 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultActiveNotificationsProviderTest.kt @@ -153,6 +153,19 @@ class DefaultActiveNotificationsProviderTest { assertThat(activeNotificationsProvider.getSummaryNotification(A_SESSION_ID_2)).isNull() } + @Test + fun `getFallbackNotification returns only the fallback notification for that session id if it exists`() { + val activeNotifications = listOf( + aStatusBarNotification(id = notificationIdProvider.getFallbackNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), + aStatusBarNotification(id = notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), groupId = A_SESSION_ID.value), + aStatusBarNotification(id = notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID_2), groupId = A_SESSION_ID_2.value), + ) + val activeNotificationsProvider = createActiveNotificationsProvider(activeNotifications = activeNotifications) + + assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID)).isNotNull() + assertThat(activeNotificationsProvider.getFallbackNotification(A_SESSION_ID_2)).isNull() + } + private fun aStatusBarNotification(id: Int, groupId: String, tag: String? = null) = mockk { every { this@mockk.id } returns id every { this@mockk.tag } returns tag diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index 58f97e55cbd..ba58db2a3cf 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -56,7 +56,6 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.services.toolbox.impl.strings.AndroidStringProvider -import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import kotlinx.coroutines.test.runTest @@ -663,7 +662,7 @@ class DefaultNotifiableEventResolverTest { roomId = A_ROOM_ID, eventId = AN_EVENT_ID, editedEventId = null, - description = "You have new messages.", + description = "", canBeReplaced = true, isRedacted = false, isUpdated = false, @@ -895,7 +894,6 @@ class DefaultNotifiableEventResolverTest { callNotificationEventResolver = callNotificationEventResolver, fallbackNotificationFactory = FallbackNotificationFactory( clock = FakeSystemClock(), - stringProvider = FakeStringProvider(defaultResult = "You have new messages.") ), featureFlagService = FakeFeatureFlagService(), ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt index 9bbc2175e59..500e31b1ace 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -39,7 +39,6 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.services.appnavstate.test.aNavigationState -import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -261,7 +260,6 @@ fun TestScope.createDefaultNotificationDrawerManager( roomGroupMessageCreator = roomGroupMessageCreator, summaryGroupMessageCreator = summaryGroupMessageCreator, activeNotificationsProvider = activeNotificationsProvider, - stringProvider = FakeStringProvider(), ), enterpriseService = enterpriseService, sessionStore = sessionStore, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt index ba17fd06835..0973ee69ce8 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultSummaryGroupMessageCreatorTest.kt @@ -39,7 +39,6 @@ class DefaultSummaryGroupMessageCreatorTest { RoomNotification( notification = Notification(), roomId = A_ROOM_ID, - summaryLine = "", messageCount = 1, latestTimestamp = A_FAKE_TIMESTAMP + 10, shouldBing = true, @@ -48,7 +47,6 @@ class DefaultSummaryGroupMessageCreatorTest { ), invitationNotifications = emptyList(), simpleNotifications = emptyList(), - fallbackNotifications = emptyList(), ) notificationCreator.createSummaryListNotificationResult.assertions() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt index 7d9d1e6550e..f897388c666 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt @@ -20,10 +20,10 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotif import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent -import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -34,6 +34,7 @@ private val MY_AVATAR_URL: String? = null private val AN_INVITATION_EVENT = anInviteNotifiableEvent(roomId = A_ROOM_ID) private val A_SIMPLE_EVENT = aSimpleNotifiableEvent(eventId = AN_EVENT_ID) private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID) +private val A_FALLBACK_EVENT = aFallbackNotifiableEvent() @RunWith(RobolectricTestRunner::class) class NotificationDataFactoryTest { @@ -47,7 +48,6 @@ class NotificationDataFactoryTest { roomGroupMessageCreator = fakeRoomGroupMessageCreator, summaryGroupMessageCreator = fakeSummaryGroupMessageCreator, activeNotificationsProvider = activeNotificationsProvider, - stringProvider = FakeStringProvider(), ) @Test @@ -64,7 +64,6 @@ class NotificationDataFactoryTest { OneShotNotification( notification = expectedNotification, tag = A_ROOM_ID.value, - summaryLine = AN_INVITATION_EVENT.description, isNoisy = AN_INVITATION_EVENT.noisy, timestamp = AN_INVITATION_EVENT.timestamp ) @@ -72,6 +71,25 @@ class NotificationDataFactoryTest { ) } + @Test + fun `given a fallback invitation when mapping to notification then it's added`() = testWith(notificationDataFactory) { + val fallbackEvents = listOf(A_FALLBACK_EVENT) + val expectedNotification = notificationCreator.createFallbackNotificationResult( + null, + aNotificationAccountParams(), + fallbackEvents, + ) + val result = toNotification(fallbackEvents, aNotificationAccountParams()) + assertThat(result).isEqualTo( + OneShotNotification( + notification = expectedNotification, + tag = "FALLBACK", + isNoisy = false, + timestamp = A_FALLBACK_EVENT.timestamp + ) + ) + } + @Test fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) { val expectedNotification = notificationCreator.createRoomInvitationNotificationResult( @@ -83,7 +101,6 @@ class NotificationDataFactoryTest { OneShotNotification( notification = expectedNotification, tag = AN_EVENT_ID.value, - summaryLine = A_SIMPLE_EVENT.description, isNoisy = A_SIMPLE_EVENT.noisy, timestamp = AN_INVITATION_EVENT.timestamp ) @@ -105,7 +122,6 @@ class NotificationDataFactoryTest { existingNotification = null, ), roomId = A_ROOM_ID, - summaryLine = "A room name: Bob Hello world!", messageCount = events.size, latestTimestamp = events.maxOf { it.timestamp }, shouldBing = events.any { it.noisy }, @@ -161,7 +177,6 @@ class NotificationDataFactoryTest { existingNotification = null, ), roomId = A_ROOM_ID, - summaryLine = "A room name: Bob Hello world!", messageCount = withRedactedRemoved.size, latestTimestamp = withRedactedRemoved.maxOf { it.timestamp }, shouldBing = withRedactedRemoved.any { it.noisy }, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt index 51d491f444c..db292fcc612 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt @@ -29,7 +29,6 @@ import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNot import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore -import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.test.runTest @@ -43,7 +42,7 @@ private const val USE_COMPLETE_NOTIFICATION_FORMAT = true private val A_SUMMARY_NOTIFICATION = SummaryNotification.Update(A_NOTIFICATION) private val ONE_SHOT_NOTIFICATION = - OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1) + OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", isNoisy = false, timestamp = -1) @RunWith(RobolectricTestRunner::class) class NotificationRendererTest { @@ -57,7 +56,6 @@ class NotificationRendererTest { roomGroupMessageCreator = roomGroupMessageCreator, summaryGroupMessageCreator = summaryGroupMessageCreator, activeNotificationsProvider = FakeActiveNotificationsProvider(), - stringProvider = FakeStringProvider(), ) private val notificationIdProvider = NotificationIdProvider diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index 0504ae433b2..ae343d811d5 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -35,8 +35,8 @@ import io.element.android.libraries.push.impl.notifications.factories.action.Acc import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.RejectInvitationActionFactory +import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent -import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider @@ -82,19 +82,11 @@ class DefaultNotificationCreatorTest { fun `test createFallbackNotification`() { val sut = createNotificationCreator() val result = sut.createFallbackNotification( + existingNotification = null, notificationAccountParams = aNotificationAccountParams(), - FallbackNotifiableEvent( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - eventId = AN_EVENT_ID, - editedEventId = null, - description = "description", - canBeReplaced = false, - isRedacted = false, - isUpdated = false, - timestamp = A_FAKE_TIMESTAMP, - cause = null, - ), + fallbackNotifiableEvents = listOf( + aFallbackNotifiableEvent(), + ) ) result.commonAssertions( expectedCategory = null, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt index ae3edce437c..2b374e5ac11 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeActiveNotificationsProvider.kt @@ -21,6 +21,7 @@ class FakeActiveNotificationsProvider( private val getMembershipNotificationForSessionResult: (SessionId) -> List = { emptyList() }, private val getMembershipNotificationForRoomResult: (SessionId, RoomId) -> List = { _, _ -> emptyList() }, private val getSummaryNotificationResult: (SessionId) -> StatusBarNotification? = { null }, + private val getFallbackNotificationResult: (SessionId) -> StatusBarNotification? = { null }, private val countResult: (SessionId) -> Int = { 0 }, ) : ActiveNotificationsProvider { override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId, threadId: ThreadId?): List { @@ -47,6 +48,10 @@ class FakeActiveNotificationsProvider( return getSummaryNotificationResult(sessionId) } + override fun getFallbackNotification(sessionId: SessionId): StatusBarNotification? { + return getFallbackNotificationResult(sessionId) + } + override fun count(sessionId: SessionId): Int { return countResult(sessionId) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt index d5e4ad96959..33071ef1ac6 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationCreator.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder import io.element.android.tests.testutils.lambda.LambdaListAnyParamsRecorder import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder +import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder import io.element.android.tests.testutils.lambda.lambdaAnyRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -34,8 +35,8 @@ class FakeNotificationCreator( lambdaRecorder { _, _ -> A_NOTIFICATION }, var createSimpleNotificationResult: LambdaTwoParamsRecorder = lambdaRecorder { _, _ -> A_NOTIFICATION }, - var createFallbackNotificationResult: LambdaTwoParamsRecorder = - lambdaRecorder { _, _ -> A_NOTIFICATION }, + var createFallbackNotificationResult: LambdaThreeParamsRecorder, Notification> = + lambdaRecorder { _, _, _ -> A_NOTIFICATION }, var createSummaryListNotificationResult: LambdaFiveParamsRecorder< NotificationAccountParams, String, Boolean, Long, NotificationAccountParams, Notification > = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION }, @@ -75,10 +76,15 @@ class FakeNotificationCreator( } override fun createFallbackNotification( + existingNotification: Notification?, notificationAccountParams: NotificationAccountParams, - fallbackNotifiableEvent: FallbackNotifiableEvent, + fallbackNotifiableEvents: List, ): Notification { - return createFallbackNotificationResult(notificationAccountParams, fallbackNotifiableEvent) + return createFallbackNotificationResult( + existingNotification, + notificationAccountParams, + fallbackNotifiableEvents, + ) } override fun createSummaryListNotification( diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt index 009513b6409..32c50ed7fd9 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDataFactory.kt @@ -19,7 +19,7 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent -import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder +import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -28,18 +28,17 @@ class FakeNotificationDataFactory( var messageEventToNotificationsResult: LambdaThreeParamsRecorder< List, ImageLoader, NotificationAccountParams, List > = lambdaRecorder { _, _, _ -> emptyList() }, - var summaryToNotificationsResult: LambdaFiveParamsRecorder< + var summaryToNotificationsResult: LambdaFourParamsRecorder< List, List, List, - List, NotificationAccountParams, SummaryNotification - > = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) }, + > = lambdaRecorder { _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) }, var inviteToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, var simpleEventToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, - var fallbackEventToNotificationsResult: LambdaOneParamRecorder, List> = - lambdaRecorder { _ -> emptyList() }, + var fallbackEventToNotificationsResult: LambdaOneParamRecorder, OneShotNotification?> = + lambdaRecorder { _ -> null }, ) : NotificationDataFactory { override suspend fun toNotifications( messages: List, @@ -69,10 +68,10 @@ class FakeNotificationDataFactory( @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( + override fun toNotification( fallback: List, notificationAccountParams: NotificationAccountParams, - ): List { + ): OneShotNotification? { return fallbackEventToNotificationsResult(fallback) } @@ -80,14 +79,12 @@ class FakeNotificationDataFactory( roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, notificationAccountParams: NotificationAccountParams, ): SummaryNotification { return summaryToNotificationsResult( roomNotifications, invitationNotifications, simpleNotifications, - fallbackNotifications, notificationAccountParams, ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt index 9db68534083..8bf309cd7b2 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt @@ -14,27 +14,25 @@ import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION -import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder +import io.element.android.tests.testutils.lambda.LambdaFourParamsRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder class FakeSummaryGroupMessageCreator( - var createSummaryNotificationResult: LambdaFiveParamsRecorder< - NotificationAccountParams, List, List, List, List, Notification> = - lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION } + var createSummaryNotificationResult: LambdaFourParamsRecorder< + NotificationAccountParams, List, List, List, Notification> = + lambdaRecorder { _, _, _, _ -> A_NOTIFICATION } ) : SummaryGroupMessageCreator { override fun createSummaryNotification( notificationAccountParams: NotificationAccountParams, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, - fallbackNotifications: List, ): Notification { return createSummaryNotificationResult( notificationAccountParams, roomNotifications, invitationNotifications, simpleNotifications, - fallbackNotifications, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt index edd0c2ba170..9b7929b6a02 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt @@ -24,10 +24,12 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_NAME_2 +import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP fun aSimpleNotifiableEvent( sessionId: SessionId = A_SESSION_ID, @@ -141,3 +143,16 @@ fun aNotifiableCallEvent( senderAvatarUrl = senderAvatarUrl, rtcNotificationType = rtcNotificationType, ) + +fun aFallbackNotifiableEvent() = FallbackNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + description = "A fallback notification", + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + timestamp = A_FAKE_TIMESTAMP, + cause = "Unable to decrypt event", +) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 7c950349857..fc072144c70 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -40,9 +40,9 @@ import io.element.android.libraries.push.impl.notifications.DefaultNotificationR import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver import io.element.android.libraries.push.impl.notifications.FallbackNotificationFactory import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels +import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent -import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.test.DefaultTestPush @@ -57,7 +57,6 @@ import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.Fa import io.element.android.libraries.workmanager.api.WorkManagerRequest import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider -import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaError @@ -627,18 +626,7 @@ class DefaultPushHandlerTest { @Test fun `when receiving a fallback event, we notify the push history service about it not being resolved`() = runTest { - val aNotifiableFallbackEvent = FallbackNotifiableEvent( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - eventId = AN_EVENT_ID, - editedEventId = null, - description = "A fallback notification", - canBeReplaced = false, - isRedacted = false, - isUpdated = false, - timestamp = 0L, - cause = "Unable to decrypt event", - ) + val aNotifiableFallbackEvent = aFallbackNotifiableEvent() val notifiableEventResult = lambdaRecorder, Result>>> { _, _ -> val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, A_PUSHER_INFO) @@ -724,7 +712,6 @@ class DefaultPushHandlerTest { appCoroutineScope = backgroundScope, fallbackNotificationFactory = FallbackNotificationFactory( clock = FakeSystemClock(), - stringProvider = FakeStringProvider(), ), syncOnNotifiableEvent = syncOnNotifiableEvent, featureFlagService = featureFlagService,