From 5c88c783165a5184ff633aa7f82d0994a51ede70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:13:11 +0000 Subject: [PATCH 01/58] chore(deps): update github artifact actions --- .github/workflows/build.yml | 4 ++-- .github/workflows/build_enterprise.yml | 2 +- .github/workflows/maestro-local.yml | 6 +++--- .github/workflows/nightlyReports.yml | 4 ++-- .github/workflows/quality.yml | 10 +++++----- .github/workflows/release.yml | 6 +++--- .github/workflows/tests.yml | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf7f262cee..6c77cb3770 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - name: Upload debug APKs if: ${{ matrix.variant == 'debug' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: elementx-debug path: | @@ -61,7 +61,7 @@ jobs: app/build/outputs/apk/fdroid/debug/*-universal-debug.apk - name: Upload x86_64 APK for Maestro if: ${{ matrix.variant == 'debug' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: elementx-apk-maestro path: | diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index 0d9b5949cc..bce9d923f3 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -61,7 +61,7 @@ jobs: run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - name: Upload debug Enterprise APKs if: ${{ matrix.variant == 'debug' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: elementx-enterprise-debug path: | diff --git a/.github/workflows/maestro-local.yml b/.github/workflows/maestro-local.yml index 7481bec0ba..ff0ac49f5c 100644 --- a/.github/workflows/maestro-local.yml +++ b/.github/workflows/maestro-local.yml @@ -44,7 +44,7 @@ jobs: ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - name: Upload APK as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: elementx-apk-maestro path: | @@ -69,7 +69,7 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.ref }} - name: Download APK artifact from previous job - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: elementx-apk-maestro - name: Enable KVM group perms @@ -102,7 +102,7 @@ jobs: script: | .github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: test-results path: | diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 537565743e..080cf99b05 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -42,7 +42,7 @@ jobs: - name: ✅ Upload kover report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: kover-results path: | @@ -74,7 +74,7 @@ jobs: run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES - name: Upload dependency analysis if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: dependency-analysis path: build/reports/dependency-check-report.html diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 5277cbdf05..1b46f1373c 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -97,7 +97,7 @@ jobs: run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon - name: Upload reports if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: konsist-report path: | @@ -174,7 +174,7 @@ jobs: run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue - name: Upload reports if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: linting-report path: | @@ -214,7 +214,7 @@ jobs: run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon - name: Upload reports if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: detekt-report path: | @@ -254,7 +254,7 @@ jobs: run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES - name: Upload reports if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ktlint-report path: | @@ -317,7 +317,7 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - name: Download reports from previous jobs - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 - name: Prepare Danger if: always() run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2cce85bd5a..29ff4a5373 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES - name: Upload bundle as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: elementx-app-gplay-bundle-unsigned path: | @@ -74,7 +74,7 @@ jobs: ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES - name: Upload bundle as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: elementx-enterprise-app-gplay-bundle-unsigned path: | @@ -102,7 +102,7 @@ jobs: ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES - name: Upload apks as artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: elementx-app-fdroid-apks-unsigned path: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 22c302cbb3..4965530b5a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,7 +61,7 @@ jobs: - name: 🚫 Upload kover failed coverage reports if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: kover-error-report path: | @@ -73,7 +73,7 @@ jobs: - name: 🚫 Upload test results on error if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: tests-and-screenshot-tests-results path: | From 031ca4f3337a5e29237b9fe2aa2f00eda6d38f51 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 25 Oct 2025 15:31:10 +0200 Subject: [PATCH 02/58] Notification: show userId in notification when several accounts are configured. --- .../enterprise/test/FakeEnterpriseService.kt | 3 +- .../android/libraries/matrix/test/TestData.kt | 4 +- .../notifications/NotificationDataFactory.kt | 41 +++++------ .../notifications/NotificationRenderer.kt | 20 ++++-- .../notifications/RoomGroupMessageCreator.kt | 14 ++-- .../SummaryGroupMessageCreator.kt | 12 ++-- .../factories/NotificationAccountParams.kt | 17 +++++ .../factories/NotificationCreator.kt | 70 +++++++++---------- .../DefaultBaseRoomGroupMessageCreatorTest.kt | 26 +++---- .../DefaultNotificationDrawerManagerTest.kt | 39 +++++++++-- ...aultOnMissedCallNotificationHandlerTest.kt | 6 +- .../DefaultSummaryGroupMessageCreatorTest.kt | 8 +-- .../NotificationDataFactoryTest.kt | 41 +++++++---- .../notifications/NotificationRendererTest.kt | 23 ++++-- .../DefaultNotificationCreatorTest.kt | 24 +++---- .../factories/NotificationAccountParams.kt | 23 ++++++ .../fake/FakeNotificationCreator.kt | 47 +++++++------ .../fake/FakeNotificationDataFactory.kt | 26 ++++--- .../fake/FakeRoomGroupMessageCreator.kt | 13 ++-- .../fake/FakeSummaryGroupMessageCreator.kt | 10 ++- 20 files changed, 265 insertions(+), 202 deletions(-) create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationAccountParams.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationAccountParams.kt diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index 39b7c320d9..72f7640615 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -24,11 +24,12 @@ class FakeEnterpriseService( private val defaultHomeserverListResult: () -> List = { emptyList() }, private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default, + initialBrandColor: Color? = null, private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, ) : EnterpriseService { - private val brandColorState = MutableStateFlow(null) + private val brandColorState = MutableStateFlow(initialBrandColor) private val semanticColorsState = MutableStateFlow(initialSemanticColors) override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 8db5fc6807..8c22c90cd7 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.test +import androidx.annotation.ColorInt import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId @@ -99,4 +100,5 @@ const val A_FORMATTED_DATE = "April 6, 1980 at 6:35 PM" const val A_LOGIN_HINT = "mxid:@alice:example.org" -const val A_COLOR_INT = 0xFF0000 +@ColorInt +const val A_COLOR_INT: Int = 0xFFFF0000.toInt() 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 a3becd94c1..329d34d5f5 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 @@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import android.graphics.Typeface import android.text.style.StyleSpan -import androidx.annotation.ColorInt import androidx.core.text.buildSpannedString import androidx.core.text.inSpans import coil3.ImageLoader @@ -19,8 +18,8 @@ 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.matrix.api.user.MatrixUser 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 @@ -31,39 +30,37 @@ import io.element.android.services.toolbox.api.strings.StringProvider interface NotificationDataFactory { suspend fun toNotifications( messages: List, - currentUser: MatrixUser, imageLoader: ImageLoader, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationInvites") @Suppress("INAPPLICABLE_JVM_NAME") fun toNotifications( invites: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationSimpleEvents") @Suppress("INAPPLICABLE_JVM_NAME") fun toNotifications( simpleEvents: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") fun toNotifications( fallback: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List fun createSummaryNotification( - currentUser: MatrixUser, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, fallbackNotifications: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): SummaryNotification } @@ -77,9 +74,8 @@ class DefaultNotificationDataFactory( ) : NotificationDataFactory { override suspend fun toNotifications( messages: List, - currentUser: MatrixUser, imageLoader: ImageLoader, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List { val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() } .groupBy { it.roomId } @@ -90,13 +86,12 @@ class DefaultNotificationDataFactory( eventsByThreadId.map { (threadId, events) -> val notification = roomGroupMessageCreator.createRoomMessage( - currentUser = currentUser, events = events, roomId = roomId, threadId = threadId, imageLoader = imageLoader, - existingNotification = getExistingNotificationForMessages(currentUser.userId, roomId, threadId), - color = color, + existingNotification = getExistingNotificationForMessages(notificationAccountParams.user.userId, roomId, threadId), + notificationAccountParams = notificationAccountParams, ) RoomNotification( notification = notification, @@ -121,12 +116,12 @@ class DefaultNotificationDataFactory( @Suppress("INAPPLICABLE_JVM_NAME") override fun toNotifications( invites: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List { return invites.map { event -> OneShotNotification( key = event.roomId.value, - notification = notificationCreator.createRoomInvitationNotification(event, color), + notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event), summaryLine = event.description, isNoisy = event.noisy, timestamp = event.timestamp @@ -138,12 +133,12 @@ class DefaultNotificationDataFactory( @Suppress("INAPPLICABLE_JVM_NAME") override fun toNotifications( simpleEvents: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List { return simpleEvents.map { event -> OneShotNotification( key = event.eventId.value, - notification = notificationCreator.createSimpleEventNotification(event, color), + notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event), summaryLine = event.description, isNoisy = event.noisy, timestamp = event.timestamp @@ -155,12 +150,12 @@ class DefaultNotificationDataFactory( @Suppress("INAPPLICABLE_JVM_NAME") override fun toNotifications( fallback: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List { return fallback.map { event -> OneShotNotification( key = event.eventId.value, - notification = notificationCreator.createFallbackNotification(event, color), + notification = notificationCreator.createFallbackNotification(notificationAccountParams, event), summaryLine = event.description.orEmpty(), isNoisy = false, timestamp = event.timestamp @@ -169,23 +164,21 @@ class DefaultNotificationDataFactory( } override fun createSummaryNotification( - currentUser: MatrixUser, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, fallbackNotifications: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): SummaryNotification { return when { roomNotifications.isEmpty() && invitationNotifications.isEmpty() && simpleNotifications.isEmpty() -> SummaryNotification.Removed else -> SummaryNotification.Update( summaryGroupMessageCreator.createSummaryNotification( - currentUser = currentUser, roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, fallbackNotifications = fallbackNotifications, - color = color, + notificationAccountParams = notificationAccountParams, ) ) } 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 5248bce175..05b72b0bcd 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 @@ -15,6 +15,7 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.api.notifications.NotificationIdProvider +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 @@ -22,6 +23,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven 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.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.first import timber.log.Timber @@ -32,6 +34,7 @@ class NotificationRenderer( private val notificationDisplayer: NotificationDisplayer, private val notificationDataFactory: NotificationDataFactory, private val enterpriseService: EnterpriseService, + private val sessionStore: SessionStore, ) { suspend fun render( currentUser: MatrixUser, @@ -41,18 +44,23 @@ class NotificationRenderer( ) { val color = enterpriseService.brandColorsFlow(currentUser.userId).first()?.toArgb() ?: NotificationConfig.NOTIFICATION_ACCENT_COLOR + val numberOfAccounts = sessionStore.getAllSessions().size + val notificationAccountParams = NotificationAccountParams( + user = currentUser, + color = color, + showSessionId = numberOfAccounts > 1, + ) val groupedEvents = eventsToProcess.groupByType() - val roomNotifications = notificationDataFactory.toNotifications(groupedEvents.roomEvents, currentUser, imageLoader, color) - val invitationNotifications = notificationDataFactory.toNotifications(groupedEvents.invitationEvents, color) - val simpleNotifications = notificationDataFactory.toNotifications(groupedEvents.simpleEvents, color) - val fallbackNotifications = notificationDataFactory.toNotifications(groupedEvents.fallbackEvents, color) + 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 summaryNotification = notificationDataFactory.createSummaryNotification( - currentUser = currentUser, roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, fallbackNotifications = fallbackNotifications, - color = color, + notificationAccountParams = notificationAccountParams, ) // Remove summary first to avoid briefly displaying it after dismissing the last notification diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 853e7ffdfc..79121db5d7 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -9,15 +9,14 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import android.graphics.Bitmap -import androidx.annotation.ColorInt 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.ThreadId -import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader 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.factories.isSmartReplyError import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -25,13 +24,12 @@ import io.element.android.services.toolbox.api.strings.StringProvider interface RoomGroupMessageCreator { suspend fun createRoomMessage( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, events: List, roomId: RoomId, threadId: ThreadId?, imageLoader: ImageLoader, existingNotification: Notification?, - @ColorInt color: Int, ): Notification } @@ -42,13 +40,12 @@ class DefaultRoomGroupMessageCreator( private val notificationCreator: NotificationCreator, ) : RoomGroupMessageCreator { override suspend fun createRoomMessage( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, events: List, roomId: RoomId, threadId: ThreadId?, imageLoader: ImageLoader, existingNotification: Notification?, - @ColorInt color: Int, ): Notification { val lastKnownRoomEvent = events.last() val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderDisambiguatedDisplayName ?: "Room name (${roomId.value.take(8)}…)" @@ -66,8 +63,9 @@ class DefaultRoomGroupMessageCreator( val smartReplyErrors = events.filter { it.isSmartReplyError() } val roomIsDm = !roomIsGroup return notificationCreator.createMessagesListNotification( + notificationAccountParams = notificationAccountParams, RoomEventGroupInfo( - sessionId = currentUser.userId, + sessionId = notificationAccountParams.user.userId, roomId = roomId, roomDisplayName = roomName, isDm = roomIsDm, @@ -80,11 +78,9 @@ class DefaultRoomGroupMessageCreator( largeIcon = largeBitmap, lastMessageTimestamp = lastMessageTimestamp, tickerText = tickerText, - currentUser = currentUser, existingNotification = existingNotification, imageLoader = imageLoader, events = events, - color = color, ) } 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 85947226f1..8600472c9d 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 @@ -8,22 +8,20 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification -import androidx.annotation.ColorInt import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import io.element.android.libraries.matrix.api.user.MatrixUser 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.services.toolbox.api.strings.StringProvider interface SummaryGroupMessageCreator { fun createSummaryNotification( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, fallbackNotifications: List, - @ColorInt color: Int, ): Notification } @@ -42,12 +40,11 @@ class DefaultSummaryGroupMessageCreator( private val notificationCreator: NotificationCreator, ) : SummaryGroupMessageCreator { override fun createSummaryNotification( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, fallbackNotifications: List, - @ColorInt color: Int, ): Notification { val summaryIsNoisy = roomNotifications.any { it.shouldBing } || invitationNotifications.any { it.isNoisy } || @@ -61,11 +58,10 @@ class DefaultSummaryGroupMessageCreator( val nbEvents = roomNotifications.size + simpleNotifications.size val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) return notificationCreator.createSummaryListNotification( - currentUser, + notificationAccountParams = notificationAccountParams, sumTitle, noisy = summaryIsNoisy, lastMessageTimestamp = lastMessageTimestamp, - color = color, ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationAccountParams.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationAccountParams.kt new file mode 100644 index 0000000000..da122b6a2a --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationAccountParams.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.push.impl.notifications.factories + +import androidx.annotation.ColorInt +import io.element.android.libraries.matrix.api.user.MatrixUser + +data class NotificationAccountParams( + val user: MatrixUser, + @ColorInt val color: Int, + val showSessionId: Boolean, +) 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 efe54bd3d9..411cb25db1 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 @@ -47,42 +47,40 @@ interface NotificationCreator { * Create a notification for a Room. */ suspend fun createMessagesListNotification( + notificationAccountParams: NotificationAccountParams, roomInfo: RoomEventGroupInfo, threadId: ThreadId?, largeIcon: Bitmap?, lastMessageTimestamp: Long, tickerText: String, - currentUser: MatrixUser, existingNotification: Notification?, imageLoader: ImageLoader, events: List, - @ColorInt color: Int, ): Notification fun createRoomInvitationNotification( + notificationAccountParams: NotificationAccountParams, inviteNotifiableEvent: InviteNotifiableEvent, - @ColorInt color: Int, ): Notification fun createSimpleEventNotification( + notificationAccountParams: NotificationAccountParams, simpleNotifiableEvent: SimpleNotifiableEvent, - @ColorInt color: Int, ): Notification fun createFallbackNotification( + notificationAccountParams: NotificationAccountParams, fallbackNotifiableEvent: FallbackNotifiableEvent, - @ColorInt color: Int, ): Notification /** * Create the summary notification. */ fun createSummaryListNotification( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, compatSummary: String, noisy: Boolean, lastMessageTimestamp: Long, - @ColorInt color: Int, ): Notification fun createDiagnosticNotification( @@ -118,16 +116,15 @@ class DefaultNotificationCreator( * Create a notification for a Room. */ override suspend fun createMessagesListNotification( + notificationAccountParams: NotificationAccountParams, roomInfo: RoomEventGroupInfo, threadId: ThreadId?, largeIcon: Bitmap?, lastMessageTimestamp: Long, tickerText: String, - currentUser: MatrixUser, existingNotification: Notification?, imageLoader: ImageLoader, events: List, - @ColorInt color: Int, ): Notification { // Build the pending intent for when the notification is clicked val eventId = events.firstOrNull()?.eventId @@ -135,7 +132,6 @@ class DefaultNotificationCreator( threadId != null -> pendingIntentFactory.createOpenThreadPendingIntent(roomInfo.sessionId, roomInfo.roomId, eventId, threadId) else -> pendingIntentFactory.createOpenRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId, eventId) } - val smallIcon = CommonDrawables.ic_notification val containsMissedCall = events.any { it.type == EventType.RTC_NOTIFICATION } val channelId = if (containsMissedCall) { notificationChannels.getChannelForIncomingCall(false) @@ -172,7 +168,7 @@ class DefaultNotificationCreator( val messagingStyle = existingNotification?.let { MessagingStyle.extractMessagingStyleFromNotification(it) } ?: messagingStyleFromCurrentUser( - user = currentUser, + user = notificationAccountParams.user, imageLoader = imageLoader, roomName = roomInfo.roomDisplayName, isThread = threadId != null, @@ -187,9 +183,7 @@ class DefaultNotificationCreator( .setWhen(lastMessageTimestamp) // MESSAGING_STYLE sets title and content for API 16 and above devices. .setStyle(messagingStyle) - .setSmallIcon(smallIcon) - // Set primary color (important for Wear 2.0 Notifications). - .setColor(color) + .configureWith(notificationAccountParams) // Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for // 'importance' which is set in the NotificationChannel. The integers representing // 'priority' are different from 'importance', so make sure you don't mix them. @@ -202,7 +196,7 @@ class DefaultNotificationCreator( setSound(it) } */ - setLights(color, 500, 500) + setLights(notificationAccountParams.color, 500, 500) } else { priority = NotificationCompat.PRIORITY_LOW } @@ -234,10 +228,9 @@ class DefaultNotificationCreator( } override fun createRoomInvitationNotification( + notificationAccountParams: NotificationAccountParams, inviteNotifiableEvent: InviteNotifiableEvent, - @ColorInt color: Int, ): Notification { - val smallIcon = CommonDrawables.ic_notification val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -245,8 +238,7 @@ class DefaultNotificationCreator( .setContentText(inviteNotifiableEvent.description.annotateForDebug(6)) .setGroup(inviteNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) - .setSmallIcon(smallIcon) - .setColor(color) + .configureWith(notificationAccountParams) .apply { addAction(rejectInvitationActionFactory.create(inviteNotifiableEvent)) addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent)) @@ -261,7 +253,7 @@ class DefaultNotificationCreator( setSound(it) } */ - setLights(color, 500, 500) + setLights(notificationAccountParams.color, 500, 500) } else { priority = NotificationCompat.PRIORITY_LOW } @@ -277,10 +269,9 @@ class DefaultNotificationCreator( } override fun createSimpleEventNotification( + notificationAccountParams: NotificationAccountParams, simpleNotifiableEvent: SimpleNotifiableEvent, - @ColorInt color: Int, ): Notification { - val smallIcon = CommonDrawables.ic_notification val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -288,8 +279,7 @@ class DefaultNotificationCreator( .setContentText(simpleNotifiableEvent.description.annotateForDebug(8)) .setGroup(simpleNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) - .setSmallIcon(smallIcon) - .setColor(color) + .configureWith(notificationAccountParams) .setAutoCancel(true) .setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(simpleNotifiableEvent.sessionId, simpleNotifiableEvent.roomId, null)) .apply { @@ -301,7 +291,7 @@ class DefaultNotificationCreator( setSound(it) } */ - setLights(color, 500, 500) + setLights(notificationAccountParams.color, 500, 500) } else { priority = NotificationCompat.PRIORITY_LOW } @@ -310,10 +300,9 @@ class DefaultNotificationCreator( } override fun createFallbackNotification( + notificationAccountParams: NotificationAccountParams, fallbackNotifiableEvent: FallbackNotifiableEvent, - @ColorInt color: Int, ): Notification { - val smallIcon = CommonDrawables.ic_notification val channelId = notificationChannels.getChannelIdForMessage(false) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) @@ -321,8 +310,7 @@ class DefaultNotificationCreator( .setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8)) .setGroup(fallbackNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) - .setSmallIcon(smallIcon) - .setColor(color) + .configureWith(notificationAccountParams) .setAutoCancel(true) .setWhen(fallbackNotifiableEvent.timestamp) // Ideally we'd use `createOpenRoomPendingIntent` here, but the broken notification might apply to an invite @@ -343,24 +331,22 @@ class DefaultNotificationCreator( * Create the summary notification. */ override fun createSummaryListNotification( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, compatSummary: String, noisy: Boolean, lastMessageTimestamp: Long, - @ColorInt color: Int, ): Notification { - val smallIcon = CommonDrawables.ic_notification val channelId = notificationChannels.getChannelIdForMessage(noisy) + val userId = notificationAccountParams.user.userId return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) // used in compat < N, after summary is built based on child notifications .setWhen(lastMessageTimestamp) .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setSmallIcon(smallIcon) - .setGroup(currentUser.userId.value) + .setGroup(userId.value) // set this notification as the summary for the group .setGroupSummary(true) - .setColor(color) + .configureWith(notificationAccountParams) .apply { if (noisy) { // Compat @@ -370,14 +356,14 @@ class DefaultNotificationCreator( setSound(it) } */ - setLights(color, 500, 500) + setLights(notificationAccountParams.color, 500, 500) } else { // compat priority = NotificationCompat.PRIORITY_LOW } } - .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(currentUser.userId)) - .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(currentUser.userId)) + .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(userId)) + .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(userId)) .build() } @@ -487,4 +473,12 @@ class DefaultNotificationCreator( } } +private fun NotificationCompat.Builder.configureWith(notificationAccountParams: NotificationAccountParams) = apply { + setSmallIcon(CommonDrawables.ic_notification) + setColor(notificationAccountParams.color) + if (notificationAccountParams.showSessionId) { + setSubText(notificationAccountParams.user.userId.value) + } +} + fun NotifiableMessageEvent.isSmartReplyError() = outGoingMessage && outGoingMessageFailed diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt index 694319c9e7..00848390b5 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt @@ -13,7 +13,6 @@ import androidx.core.app.NotificationCompat import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.NotificationConfig import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.test.A_COLOR_INT import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.ui.components.aMatrixUser @@ -21,6 +20,7 @@ import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIX import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE +import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.test.notifications.FakeImageLoader @@ -44,7 +44,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { val sut = createRoomGroupMessageCreator() val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( - currentUser = aMatrixUser(), + notificationAccountParams = aNotificationAccountParams(), events = listOf( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( imageUriString = "aUri", @@ -54,7 +54,6 @@ class DefaultBaseRoomGroupMessageCreatorTest { imageLoader = fakeImageLoader.getImageLoader(), existingNotification = null, threadId = null, - color = A_COLOR_INT, ) assertThat(result.number).isEqualTo(1) @Suppress("DEPRECATION") @@ -68,7 +67,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { val sut = createRoomGroupMessageCreator() val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( - currentUser = aMatrixUser(), + notificationAccountParams = aNotificationAccountParams(), events = listOf( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( noisy = true, @@ -78,7 +77,6 @@ class DefaultBaseRoomGroupMessageCreatorTest { imageLoader = fakeImageLoader.getImageLoader(), existingNotification = null, threadId = null, - color = A_COLOR_INT, ) @Suppress("DEPRECATION") assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT) @@ -130,9 +128,11 @@ class DefaultBaseRoomGroupMessageCreatorTest { sdkIntProvider = FakeBuildVersionSdkIntProvider(api) ) val result = sut.createRoomMessage( - currentUser = aMatrixUser( - // Some user avatar - avatarUrl = A_USER_AVATAR_1, + notificationAccountParams = aNotificationAccountParams( + user = aMatrixUser( + // Some user avatar + avatarUrl = A_USER_AVATAR_1, + ) ), events = listOf( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( @@ -144,7 +144,6 @@ class DefaultBaseRoomGroupMessageCreatorTest { imageLoader = fakeImageLoader.getImageLoader(), existingNotification = null, threadId = null, - color = A_COLOR_INT, ) assertThat(result.number).isEqualTo(1) assertThat(fakeImageLoader.getCoilRequests()).containsExactlyElementsIn(expectedCoilRequests) @@ -155,7 +154,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { val sut = createRoomGroupMessageCreator() val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( - currentUser = aMatrixUser(), + notificationAccountParams = aNotificationAccountParams(), events = listOf( aNotifiableMessageEvent(timestamp = A_TIMESTAMP), aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10), @@ -164,7 +163,6 @@ class DefaultBaseRoomGroupMessageCreatorTest { imageLoader = fakeImageLoader.getImageLoader(), existingNotification = null, threadId = null, - color = A_COLOR_INT, ) assertThat(result.number).isEqualTo(2) assertThat(result.`when`).isEqualTo(A_TIMESTAMP + 10) @@ -183,7 +181,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { val sut = createRoomGroupMessageCreator() val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( - currentUser = aMatrixUser(), + notificationAccountParams = aNotificationAccountParams(), events = listOf( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( outGoingMessage = true, @@ -194,7 +192,6 @@ class DefaultBaseRoomGroupMessageCreatorTest { imageLoader = fakeImageLoader.getImageLoader(), existingNotification = null, threadId = null, - color = A_COLOR_INT, ) val actionTitles = result.actions?.map { it.title } assertThat(actionTitles).isEqualTo( @@ -210,7 +207,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { val sut = createRoomGroupMessageCreator() val fakeImageLoader = FakeImageLoader() val result = sut.createRoomMessage( - currentUser = aMatrixUser(), + notificationAccountParams = aNotificationAccountParams(), events = listOf( aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( roomIsDm = true, @@ -220,7 +217,6 @@ class DefaultBaseRoomGroupMessageCreatorTest { imageLoader = fakeImageLoader.getImageLoader(), existingNotification = null, threadId = null, - color = A_COLOR_INT, ) assertThat(result.number).isEqualTo(1) assertThat(result.`when`).isEqualTo(A_TIMESTAMP) 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 b2a4cc1c9b..275c1ac490 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 @@ -8,8 +8,10 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification +import androidx.compose.ui.graphics.Color import androidx.core.app.NotificationManagerCompat import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -21,12 +23,15 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.push.api.notifications.NotificationIdProvider +import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider 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.aNotifiableMessageEvent import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState @@ -64,8 +69,8 @@ class DefaultNotificationDrawerManagerTest { // For now just call all the API. Later, add more valuable tests. val matrixUser = aMatrixUser(id = A_SESSION_ID.value, displayName = "alice", avatarUrl = "mxc://data") val mockRoomGroupMessageCreator = FakeRoomGroupMessageCreator( - createRoomMessageResult = lambdaRecorder { user, _, roomId, _, _, existingNotification -> - assertThat(user).isEqualTo(matrixUser) + createRoomMessageResult = lambdaRecorder { notificationAccountParams, _, roomId, _, _, existingNotification -> + assertThat(notificationAccountParams.user).isEqualTo(matrixUser) assertThat(roomId).isEqualTo(A_ROOM_ID) assertThat(existingNotification).isNull() Notification() @@ -128,6 +133,9 @@ class DefaultNotificationDrawerManagerTest { val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( matrixClientProvider = matrixClientProvider, roomGroupMessageCreator = messageCreator, + enterpriseService = FakeEnterpriseService( + initialBrandColor = Color.Red, + ) ) // Gets a display name from MatrixClient.getUserProfile matrixClient.givenGetProfileResult(A_SESSION_ID, Result.success(aMatrixUser(id = A_SESSION_ID.value, displayName = "alice"))) @@ -144,15 +152,29 @@ class DefaultNotificationDrawerManagerTest { messageCreator.createRoomMessageResult.assertions() .isCalledExactly(3) .withSequence( - listOf(value(aMatrixUser(id = A_SESSION_ID.value, displayName = "alice")), any(), any(), any(), any(), any()), - listOf(value(aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value)), any(), any(), any(), any(), any()), listOf( - value(aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value, avatarUrl = AN_AVATAR_URL)), + value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = "alice"))), + any(), + any(), + any(), + any(), + any(), + ), + listOf( + value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value))), + any(), + any(), + any(), + any(), + any(), + ), + listOf( + value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value, avatarUrl = AN_AVATAR_URL))), + any(), any(), any(), any(), any(), - any() ), ) @@ -194,6 +216,8 @@ class DefaultNotificationDrawerManagerTest { summaryGroupMessageCreator: SummaryGroupMessageCreator = FakeSummaryGroupMessageCreator(), activeNotificationsProvider: FakeActiveNotificationsProvider = FakeActiveNotificationsProvider(), matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), + sessionStore: SessionStore = InMemorySessionStore(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), ): DefaultNotificationDrawerManager { val context = RuntimeEnvironment.getApplication() return DefaultNotificationDrawerManager( @@ -207,7 +231,8 @@ class DefaultNotificationDrawerManagerTest { activeNotificationsProvider = activeNotificationsProvider, stringProvider = FakeStringProvider(), ), - enterpriseService = FakeEnterpriseService(), + enterpriseService = enterpriseService, + sessionStore = sessionStore, ), appNavigationStateService = appNavigationStateService, coroutineScope = this, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt index 8b668a7f77..e5a4b01043 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.push.impl.notifications -import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -18,7 +17,6 @@ import io.element.android.libraries.matrix.test.notification.FakeNotificationSer import io.element.android.libraries.matrix.test.notification.aNotificationData import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory -import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder @@ -53,10 +51,8 @@ class DefaultOnMissedCallNotificationHandlerTest { matrixClientProvider = matrixClientProvider, defaultNotificationDrawerManager = DefaultNotificationDrawerManager( notificationManager = mockk(relaxed = true), - notificationRenderer = NotificationRenderer( - notificationDisplayer = FakeNotificationDisplayer(), + notificationRenderer = createNotificationRenderer( notificationDataFactory = dataFactory, - enterpriseService = FakeEnterpriseService(), ), appNavigationStateService = FakeAppNavigationStateService(), coroutineScope = backgroundScope, 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 e34ea0848c..f701dae6c7 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 @@ -10,9 +10,8 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import androidx.core.app.NotificationCompat import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.test.A_COLOR_INT import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP @@ -34,7 +33,7 @@ class DefaultSummaryGroupMessageCreatorTest { ) val result = summaryCreator.createSummaryNotification( - currentUser = aMatrixUser(), + notificationAccountParams = aNotificationAccountParams(), roomNotifications = listOf( RoomNotification( notification = Notification(), @@ -49,12 +48,11 @@ class DefaultSummaryGroupMessageCreatorTest { invitationNotifications = emptyList(), simpleNotifications = emptyList(), fallbackNotifications = emptyList(), - color = A_COLOR_INT, ) notificationCreator.createSummaryListNotificationResult.assertions() .isCalledOnce() - .with(any(), nonNull(), any(), any()) + .with(any(), any(), nonNull(), any(), any()) // Set from the events included @Suppress("DEPRECATION") 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 f563106c14..2b11ce949b 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 @@ -11,9 +11,9 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.matrix.test.A_COLOR_INT import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator @@ -51,10 +51,13 @@ class NotificationDataFactoryTest { @Test fun `given a room invitation when mapping to notification then it's added`() = testWith(notificationDataFactory) { - val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(AN_INVITATION_EVENT) + val expectedNotification = notificationCreator.createRoomInvitationNotificationResult( + aNotificationAccountParams(), + AN_INVITATION_EVENT, + ) val roomInvitation = listOf(AN_INVITATION_EVENT) - val result = toNotifications(roomInvitation, A_COLOR_INT) + val result = toNotifications(roomInvitation, aNotificationAccountParams()) assertThat(result).isEqualTo( listOf( @@ -71,10 +74,13 @@ class NotificationDataFactoryTest { @Test fun `given a simple event when mapping to notification then it's added`() = testWith(notificationDataFactory) { - val expectedNotification = notificationCreator.createRoomInvitationNotificationResult(AN_INVITATION_EVENT) + val expectedNotification = notificationCreator.createRoomInvitationNotificationResult( + aNotificationAccountParams(), + AN_INVITATION_EVENT, + ) val roomInvitation = listOf(A_SIMPLE_EVENT) - val result = toNotifications(roomInvitation, A_COLOR_INT) + val result = toNotifications(roomInvitation, aNotificationAccountParams()) assertThat(result).isEqualTo( listOf( @@ -94,13 +100,14 @@ class NotificationDataFactoryTest { val events = listOf(A_MESSAGE_EVENT) val expectedNotification = RoomNotification( notification = fakeRoomGroupMessageCreator.createRoomMessage( - currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + notificationAccountParams = aNotificationAccountParams( + user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + ), events = events, roomId = A_ROOM_ID, threadId = null, imageLoader = FakeImageLoader().getImageLoader(), existingNotification = null, - color = A_COLOR_INT, ), roomId = A_ROOM_ID, summaryLine = "A room name: Bob Hello world!", @@ -113,10 +120,11 @@ class NotificationDataFactoryTest { val fakeImageLoader = FakeImageLoader() val result = toNotifications( + notificationAccountParams = aNotificationAccountParams( + user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + ), messages = roomWithMessage, - currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), imageLoader = fakeImageLoader.getImageLoader(), - color = A_COLOR_INT, ) assertThat(result.size).isEqualTo(1) @@ -130,10 +138,11 @@ class NotificationDataFactoryTest { val fakeImageLoader = FakeImageLoader() val result = toNotifications( + notificationAccountParams = aNotificationAccountParams( + user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + ), messages = redactedRoom, - currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), imageLoader = fakeImageLoader.getImageLoader(), - color = A_COLOR_INT, ) assertThat(result).isEmpty() @@ -151,13 +160,14 @@ class NotificationDataFactoryTest { val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted"))) val expectedNotification = RoomNotification( notification = fakeRoomGroupMessageCreator.createRoomMessage( - currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + notificationAccountParams = aNotificationAccountParams( + user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + ), events = withRedactedRemoved, roomId = A_ROOM_ID, threadId = null, imageLoader = FakeImageLoader().getImageLoader(), existingNotification = null, - color = A_COLOR_INT, ), roomId = A_ROOM_ID, summaryLine = "A room name: Bob Hello world!", @@ -169,10 +179,11 @@ class NotificationDataFactoryTest { val fakeImageLoader = FakeImageLoader() val result = toNotifications( + notificationAccountParams = aNotificationAccountParams( + user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), + ), messages = roomWithRedactedMessage, - currentUser = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), imageLoader = fakeImageLoader.getImageLoader(), - color = A_COLOR_INT, ) assertThat(result.size).isEqualTo(1) 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 589d7876f5..1026dca849 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 @@ -7,6 +7,7 @@ package io.element.android.libraries.push.impl.notifications +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -15,6 +16,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator +import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator @@ -24,6 +26,8 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNoti import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.test.notifications.FakeImageLoader +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 @@ -56,10 +60,9 @@ class NotificationRendererTest { ) private val notificationIdProvider = NotificationIdProvider - private val notificationRenderer = NotificationRenderer( + private val notificationRenderer = createNotificationRenderer( notificationDisplayer = notificationDisplayer, notificationDataFactory = notificationDataFactory, - enterpriseService = FakeEnterpriseService(), ) @Test @@ -83,7 +86,7 @@ class NotificationRendererTest { @Test fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest { - notificationCreator.createSimpleNotificationResult = lambdaRecorder { _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification } + notificationCreator.createSimpleNotificationResult = lambdaRecorder { _, _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification } renderEventsAsNotifications(listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID))) @@ -95,7 +98,7 @@ class NotificationRendererTest { @Test fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest { - notificationCreator.createRoomInvitationNotificationResult = lambdaRecorder { _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification } + notificationCreator.createRoomInvitationNotificationResult = lambdaRecorder { _, _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification } renderEventsAsNotifications(listOf(anInviteNotifiableEvent())) @@ -114,3 +117,15 @@ class NotificationRendererTest { ) } } + +fun createNotificationRenderer( + notificationDisplayer: NotificationDisplayer = FakeNotificationDisplayer(), + notificationDataFactory: NotificationDataFactory = FakeNotificationDataFactory(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), + sessionStore: SessionStore = InMemorySessionStore(), +) = NotificationRenderer( + notificationDisplayer = notificationDisplayer, + notificationDataFactory = notificationDataFactory, + enterpriseService = enterpriseService, + sessionStore = sessionStore, +) 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 045cc0492d..df9f106972 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 @@ -65,6 +65,7 @@ class DefaultNotificationCreatorTest { fun `test createFallbackNotification`() { val sut = createNotificationCreator() val result = sut.createFallbackNotification( + notificationAccountParams = aNotificationAccountParams(), FallbackNotifiableEvent( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, @@ -77,7 +78,6 @@ class DefaultNotificationCreatorTest { timestamp = A_FAKE_TIMESTAMP, cause = null, ), - color = A_COLOR_INT, ) result.commonAssertions( expectedCategory = null, @@ -88,6 +88,7 @@ class DefaultNotificationCreatorTest { fun `test createSimpleEventNotification`() { val sut = createNotificationCreator() val result = sut.createSimpleEventNotification( + notificationAccountParams = aNotificationAccountParams(), SimpleNotifiableEvent( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, @@ -103,7 +104,6 @@ class DefaultNotificationCreatorTest { isRedacted = false, isUpdated = false, ), - color = A_COLOR_INT, ) result.commonAssertions( expectedCategory = null, @@ -114,6 +114,7 @@ class DefaultNotificationCreatorTest { fun `test createSimpleEventNotification noisy`() { val sut = createNotificationCreator() val result = sut.createSimpleEventNotification( + notificationAccountParams = aNotificationAccountParams(), SimpleNotifiableEvent( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, @@ -129,7 +130,6 @@ class DefaultNotificationCreatorTest { isRedacted = false, isUpdated = false, ), - color = A_COLOR_INT, ) result.commonAssertions( expectedCategory = null, @@ -140,6 +140,7 @@ class DefaultNotificationCreatorTest { fun `test createRoomInvitationNotification`() { val sut = createNotificationCreator() val result = sut.createRoomInvitationNotification( + notificationAccountParams = aNotificationAccountParams(), InviteNotifiableEvent( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, @@ -156,7 +157,6 @@ class DefaultNotificationCreatorTest { isUpdated = false, roomName = "roomName", ), - color = A_COLOR_INT, ) result.commonAssertions( expectedCategory = null, @@ -174,6 +174,7 @@ class DefaultNotificationCreatorTest { fun `test createRoomInvitationNotification noisy`() { val sut = createNotificationCreator() val result = sut.createRoomInvitationNotification( + notificationAccountParams = aNotificationAccountParams(), InviteNotifiableEvent( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, @@ -190,7 +191,6 @@ class DefaultNotificationCreatorTest { isUpdated = false, roomName = "roomName", ), - color = A_COLOR_INT, ) result.commonAssertions( expectedCategory = null, @@ -202,11 +202,10 @@ class DefaultNotificationCreatorTest { val sut = createNotificationCreator() val matrixUser = aMatrixUser() val result = sut.createSummaryListNotification( - currentUser = matrixUser, + notificationAccountParams = aNotificationAccountParams(user = matrixUser), compatSummary = "compatSummary", noisy = false, lastMessageTimestamp = 123_456L, - color = A_COLOR_INT, ) result.commonAssertions( expectedGroup = matrixUser.userId.value, @@ -218,11 +217,10 @@ class DefaultNotificationCreatorTest { val sut = createNotificationCreator() val matrixUser = aMatrixUser() val result = sut.createSummaryListNotification( - currentUser = matrixUser, + notificationAccountParams = aNotificationAccountParams(user = matrixUser), compatSummary = "compatSummary", noisy = true, lastMessageTimestamp = 123_456L, - color = A_COLOR_INT, ) result.commonAssertions( expectedGroup = matrixUser.userId.value, @@ -232,8 +230,8 @@ class DefaultNotificationCreatorTest { @Test fun `test createMessagesListNotification`() = runTest { val sut = createNotificationCreator() - aMatrixUser() val result = sut.createMessagesListNotification( + notificationAccountParams = aNotificationAccountParams(), roomInfo = RoomEventGroupInfo( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, @@ -247,11 +245,9 @@ class DefaultNotificationCreatorTest { largeIcon = null, lastMessageTimestamp = 123_456L, tickerText = "tickerText", - currentUser = aMatrixUser(), existingNotification = null, imageLoader = FakeImageLoader().getImageLoader(), events = listOf(aNotifiableMessageEvent()), - color = A_COLOR_INT, ) result.commonAssertions() } @@ -259,8 +255,8 @@ class DefaultNotificationCreatorTest { @Test fun `test createMessagesListNotification should bing and thread`() = runTest { val sut = createNotificationCreator() - aMatrixUser() val result = sut.createMessagesListNotification( + notificationAccountParams = aNotificationAccountParams(), roomInfo = RoomEventGroupInfo( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, @@ -274,11 +270,9 @@ class DefaultNotificationCreatorTest { largeIcon = null, lastMessageTimestamp = 123_456L, tickerText = "tickerText", - currentUser = aMatrixUser(), existingNotification = null, imageLoader = FakeImageLoader().getImageLoader(), events = listOf(aNotifiableMessageEvent()), - color = A_COLOR_INT, ) result.commonAssertions() } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationAccountParams.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationAccountParams.kt new file mode 100644 index 0000000000..eca910b07e --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationAccountParams.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.push.impl.notifications.factories + +import androidx.annotation.ColorInt +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.test.A_COLOR_INT +import io.element.android.libraries.matrix.ui.components.aMatrixUser + +fun aNotificationAccountParams( + user: MatrixUser = aMatrixUser(), + @ColorInt color: Int = A_COLOR_INT, + showSessionId: Boolean = false, +) = NotificationAccountParams( + user = user, + color = color, + showSessionId = showSessionId, +) 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 1cc4468b15..3a4acb757f 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 @@ -12,81 +12,84 @@ import android.graphics.Bitmap import androidx.annotation.ColorInt import coil3.ImageLoader import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo +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.fixtures.A_NOTIFICATION 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.tests.testutils.lambda.LambdaFourParamsRecorder +import io.element.android.tests.testutils.lambda.LambdaFiveParamsRecorder import io.element.android.tests.testutils.lambda.LambdaListAnyParamsRecorder -import io.element.android.tests.testutils.lambda.LambdaNoParamRecorder import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder +import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder import io.element.android.tests.testutils.lambda.lambdaAnyRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder class FakeNotificationCreator( var createMessagesListNotificationResult: LambdaListAnyParamsRecorder = lambdaAnyRecorder { A_NOTIFICATION }, - var createRoomInvitationNotificationResult: LambdaOneParamRecorder = lambdaRecorder { _ -> A_NOTIFICATION }, - var createSimpleNotificationResult: LambdaOneParamRecorder = lambdaRecorder { _ -> A_NOTIFICATION }, - var createFallbackNotificationResult: LambdaOneParamRecorder = lambdaRecorder { _ -> A_NOTIFICATION }, - var createSummaryListNotificationResult: LambdaFourParamsRecorder = - lambdaRecorder { _, _, _, _ -> A_NOTIFICATION }, - var createDiagnosticNotificationResult: LambdaNoParamRecorder = lambdaRecorder { A_NOTIFICATION }, + var createRoomInvitationNotificationResult: LambdaTwoParamsRecorder = + lambdaRecorder { _, _ -> A_NOTIFICATION }, + var createSimpleNotificationResult: LambdaTwoParamsRecorder = + lambdaRecorder { _, _ -> A_NOTIFICATION }, + var createFallbackNotificationResult: LambdaTwoParamsRecorder = + lambdaRecorder { _, _ -> A_NOTIFICATION }, + var createSummaryListNotificationResult: LambdaFiveParamsRecorder< + NotificationAccountParams, String, Boolean, Long, NotificationAccountParams, Notification + > = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION }, + var createDiagnosticNotificationResult: LambdaOneParamRecorder = + lambdaRecorder { _ -> A_NOTIFICATION }, ) : NotificationCreator { override suspend fun createMessagesListNotification( + notificationAccountParams: NotificationAccountParams, roomInfo: RoomEventGroupInfo, threadId: ThreadId?, largeIcon: Bitmap?, lastMessageTimestamp: Long, tickerText: String, - currentUser: MatrixUser, existingNotification: Notification?, imageLoader: ImageLoader, events: List, - @ColorInt color: Int, ): Notification { return createMessagesListNotificationResult( - listOf(roomInfo, threadId, largeIcon, lastMessageTimestamp, tickerText, currentUser, existingNotification, imageLoader, events) + listOf(notificationAccountParams, roomInfo, threadId, largeIcon, lastMessageTimestamp, tickerText, existingNotification, imageLoader, events) ) } override fun createRoomInvitationNotification( + notificationAccountParams: NotificationAccountParams, inviteNotifiableEvent: InviteNotifiableEvent, - @ColorInt color: Int, ): Notification { - return createRoomInvitationNotificationResult(inviteNotifiableEvent) + return createRoomInvitationNotificationResult(notificationAccountParams, inviteNotifiableEvent) } override fun createSimpleEventNotification( + notificationAccountParams: NotificationAccountParams, simpleNotifiableEvent: SimpleNotifiableEvent, - @ColorInt color: Int, ): Notification { - return createSimpleNotificationResult(simpleNotifiableEvent) + return createSimpleNotificationResult(notificationAccountParams, simpleNotifiableEvent) } override fun createFallbackNotification( + notificationAccountParams: NotificationAccountParams, fallbackNotifiableEvent: FallbackNotifiableEvent, - @ColorInt color: Int, ): Notification { - return createFallbackNotificationResult(fallbackNotifiableEvent) + return createFallbackNotificationResult(notificationAccountParams, fallbackNotifiableEvent) } override fun createSummaryListNotification( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, compatSummary: String, noisy: Boolean, lastMessageTimestamp: Long, - @ColorInt color: Int, ): Notification { - return createSummaryListNotificationResult(currentUser, compatSummary, noisy, lastMessageTimestamp) + return createSummaryListNotificationResult(notificationAccountParams, compatSummary, noisy, lastMessageTimestamp, notificationAccountParams) } override fun createDiagnosticNotification( @ColorInt color: Int, ): Notification { - return createDiagnosticNotificationResult() + return createDiagnosticNotificationResult(color) } } 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 9a0a5fe7ef..a897dbfc09 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 @@ -7,13 +7,12 @@ package io.element.android.libraries.push.impl.notifications.fake -import androidx.annotation.ColorInt import coil3.ImageLoader -import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.NotificationDataFactory import io.element.android.libraries.push.impl.notifications.OneShotNotification import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.SummaryNotification +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.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent @@ -25,14 +24,15 @@ import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder class FakeNotificationDataFactory( - var messageEventToNotificationsResult: LambdaThreeParamsRecorder, MatrixUser, ImageLoader, List> = - lambdaRecorder { _, _, _ -> emptyList() }, + var messageEventToNotificationsResult: LambdaThreeParamsRecorder< + List, ImageLoader, NotificationAccountParams, List + > = lambdaRecorder { _, _, _ -> emptyList() }, var summaryToNotificationsResult: LambdaFiveParamsRecorder< - MatrixUser, List, List, List, List, + NotificationAccountParams, SummaryNotification > = lambdaRecorder { _, _, _, _, _ -> SummaryNotification.Update(A_NOTIFICATION) }, var inviteToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, @@ -42,18 +42,17 @@ class FakeNotificationDataFactory( ) : NotificationDataFactory { override suspend fun toNotifications( messages: List, - currentUser: MatrixUser, imageLoader: ImageLoader, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List { - return messageEventToNotificationsResult(messages, currentUser, imageLoader) + return messageEventToNotificationsResult(messages, imageLoader, notificationAccountParams) } @JvmName("toNotificationInvites") @Suppress("INAPPLICABLE_JVM_NAME") override fun toNotifications( invites: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List { return inviteToNotificationsResult(invites) } @@ -62,7 +61,7 @@ class FakeNotificationDataFactory( @Suppress("INAPPLICABLE_JVM_NAME") override fun toNotifications( simpleEvents: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List { return simpleEventToNotificationsResult(simpleEvents) } @@ -71,25 +70,24 @@ class FakeNotificationDataFactory( @Suppress("INAPPLICABLE_JVM_NAME") override fun toNotifications( fallback: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): List { return fallbackEventToNotificationsResult(fallback) } override fun createSummaryNotification( - currentUser: MatrixUser, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, fallbackNotifications: List, - @ColorInt color: Int, + notificationAccountParams: NotificationAccountParams, ): SummaryNotification { return summaryToNotificationsResult( - currentUser, roomNotifications, invitationNotifications, simpleNotifications, fallbackNotifications, + notificationAccountParams, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt index 351300937b..6bb5b63977 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt @@ -8,12 +8,11 @@ package io.element.android.libraries.push.impl.notifications.fake import android.app.Notification -import androidx.annotation.ColorInt import coil3.ImageLoader import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator +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.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.tests.testutils.lambda.LambdaSixParamsRecorder @@ -22,18 +21,18 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder // We just can't make the param types fit @Suppress("MaxLineLength", "ktlint:standard:max-line-length", "ktlint:standard:parameter-wrapping") class FakeRoomGroupMessageCreator( - var createRoomMessageResult: LambdaSixParamsRecorder, RoomId, ThreadId?, ImageLoader, Notification?, Notification> = - lambdaRecorder { _, _, _, _, _, _ -> A_NOTIFICATION } + var createRoomMessageResult: LambdaSixParamsRecorder< + NotificationAccountParams, List, RoomId, ThreadId?, ImageLoader, Notification?, Notification + > = lambdaRecorder { _, _, _, _, _, _ -> A_NOTIFICATION } ) : RoomGroupMessageCreator { override suspend fun createRoomMessage( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, events: List, roomId: RoomId, threadId: ThreadId?, imageLoader: ImageLoader, existingNotification: Notification?, - @ColorInt color: Int, ): Notification { - return createRoomMessageResult(currentUser, events, roomId, threadId, imageLoader, existingNotification) + return createRoomMessageResult(notificationAccountParams, events, roomId, threadId, imageLoader, existingNotification) } } 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 bc8a5515c9..8a1e8bb4ed 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 @@ -8,30 +8,28 @@ package io.element.android.libraries.push.impl.notifications.fake import android.app.Notification -import androidx.annotation.ColorInt -import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.OneShotNotification 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.lambdaRecorder class FakeSummaryGroupMessageCreator( var createSummaryNotificationResult: LambdaFiveParamsRecorder< - MatrixUser, List, List, List, List, Notification> = + NotificationAccountParams, List, List, List, List, Notification> = lambdaRecorder { _, _, _, _, _ -> A_NOTIFICATION } ) : SummaryGroupMessageCreator { override fun createSummaryNotification( - currentUser: MatrixUser, + notificationAccountParams: NotificationAccountParams, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, fallbackNotifications: List, - @ColorInt color: Int, ): Notification { return createSummaryNotificationResult( - currentUser, + notificationAccountParams, roomNotifications, invitationNotifications, simpleNotifications, From b0e5e6cc616a1561aa4e256821f67cc206ada6df Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 26 Oct 2025 08:49:48 +0100 Subject: [PATCH 03/58] NotificationDataFactory: improve API --- .../notifications/NotificationDataFactory.kt | 32 ++++++--------- .../notifications/NotificationRenderer.kt | 16 ++++++-- .../NotificationDataFactoryTest.kt | 41 ++++++------------- .../fake/FakeNotificationDataFactory.kt | 20 ++++----- 4 files changed, 45 insertions(+), 64 deletions(-) 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 329d34d5f5..25cac5922d 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 @@ -28,30 +28,26 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import io.element.android.services.toolbox.api.strings.StringProvider interface NotificationDataFactory { - suspend fun toNotifications( - messages: List, + suspend fun List.toNotifications( imageLoader: ImageLoader, notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationInvites") @Suppress("INAPPLICABLE_JVM_NAME") - fun toNotifications( - invites: List, + fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationSimpleEvents") @Suppress("INAPPLICABLE_JVM_NAME") - fun toNotifications( - simpleEvents: List, + fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - fun toNotifications( - fallback: List, + fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List @@ -72,12 +68,11 @@ class DefaultNotificationDataFactory( private val activeNotificationsProvider: ActiveNotificationsProvider, private val stringProvider: StringProvider, ) : NotificationDataFactory { - override suspend fun toNotifications( - messages: List, + override suspend fun List.toNotifications( imageLoader: ImageLoader, notificationAccountParams: NotificationAccountParams, ): List { - val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() } + val messagesToDisplay = filterNot { it.canNotBeDisplayed() } .groupBy { it.roomId } return messagesToDisplay.flatMap { (roomId, events) -> val roomName = events.lastOrNull()?.roomName ?: roomId.value @@ -114,11 +109,10 @@ class DefaultNotificationDataFactory( @JvmName("toNotificationInvites") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( - invites: List, + override fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List { - return invites.map { event -> + return map { event -> OneShotNotification( key = event.roomId.value, notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event), @@ -131,11 +125,10 @@ class DefaultNotificationDataFactory( @JvmName("toNotificationSimpleEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( - simpleEvents: List, + override fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List { - return simpleEvents.map { event -> + return map { event -> OneShotNotification( key = event.eventId.value, notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event), @@ -148,11 +141,10 @@ class DefaultNotificationDataFactory( @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( - fallback: List, + override fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List { - return fallback.map { event -> + return map { event -> OneShotNotification( key = event.eventId.value, notification = notificationCreator.createFallbackNotification(notificationAccountParams, event), 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 05b72b0bcd..4d6e588726 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 @@ -51,10 +51,18 @@ class NotificationRenderer( showSessionId = numberOfAccounts > 1, ) val groupedEvents = eventsToProcess.groupByType() - 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 roomNotifications = with(notificationDataFactory) { + groupedEvents.roomEvents.toNotifications(imageLoader, notificationAccountParams) + } + val invitationNotifications = with(notificationDataFactory) { + groupedEvents.invitationEvents.toNotifications(notificationAccountParams) + } + val simpleNotifications = with(notificationDataFactory) { + groupedEvents.simpleEvents.toNotifications(notificationAccountParams) + } + val fallbackNotifications = with(notificationDataFactory) { + groupedEvents.fallbackEvents.toNotifications(notificationAccountParams) + } val summaryNotification = notificationDataFactory.createSummaryNotification( roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, 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 2b11ce949b..c8a7e6ba33 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 @@ -55,10 +55,7 @@ class NotificationDataFactoryTest { aNotificationAccountParams(), AN_INVITATION_EVENT, ) - val roomInvitation = listOf(AN_INVITATION_EVENT) - - val result = toNotifications(roomInvitation, aNotificationAccountParams()) - + val result = listOf(AN_INVITATION_EVENT).toNotifications(aNotificationAccountParams()) assertThat(result).isEqualTo( listOf( OneShotNotification( @@ -78,19 +75,14 @@ class NotificationDataFactoryTest { aNotificationAccountParams(), AN_INVITATION_EVENT, ) - val roomInvitation = listOf(A_SIMPLE_EVENT) - - val result = toNotifications(roomInvitation, aNotificationAccountParams()) - - assertThat(result).isEqualTo( - listOf( - OneShotNotification( - notification = expectedNotification, - key = AN_EVENT_ID.value, - summaryLine = A_SIMPLE_EVENT.description, - isNoisy = A_SIMPLE_EVENT.noisy, - timestamp = AN_INVITATION_EVENT.timestamp - ) + val result = listOf(A_SIMPLE_EVENT).toNotifications(aNotificationAccountParams()) + assertThat(result).containsExactly( + OneShotNotification( + notification = expectedNotification, + key = AN_EVENT_ID.value, + summaryLine = A_SIMPLE_EVENT.description, + isNoisy = A_SIMPLE_EVENT.noisy, + timestamp = AN_INVITATION_EVENT.timestamp ) ) } @@ -116,14 +108,11 @@ class NotificationDataFactoryTest { shouldBing = events.any { it.noisy }, threadId = null, ) - val roomWithMessage = listOf(A_MESSAGE_EVENT) - val fakeImageLoader = FakeImageLoader() - val result = toNotifications( + val result = listOf(A_MESSAGE_EVENT).toNotifications( notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), - messages = roomWithMessage, imageLoader = fakeImageLoader.getImageLoader(), ) @@ -134,17 +123,14 @@ class NotificationDataFactoryTest { @Test fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationDataFactory) { - val redactedRoom = listOf(A_MESSAGE_EVENT.copy(isRedacted = true)) - + val redactedRoom = A_MESSAGE_EVENT.copy(isRedacted = true) val fakeImageLoader = FakeImageLoader() - val result = toNotifications( + val result = listOf(redactedRoom).toNotifications( notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), - messages = redactedRoom, imageLoader = fakeImageLoader.getImageLoader(), ) - assertThat(result).isEmpty() assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) } @@ -178,11 +164,10 @@ class NotificationDataFactoryTest { ) val fakeImageLoader = FakeImageLoader() - val result = toNotifications( + val result = roomWithRedactedMessage.toNotifications( notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), - messages = roomWithRedactedMessage, imageLoader = fakeImageLoader.getImageLoader(), ) 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 a897dbfc09..89326f5ad9 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 @@ -40,39 +40,35 @@ class FakeNotificationDataFactory( var fallbackEventToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, ) : NotificationDataFactory { - override suspend fun toNotifications( - messages: List, + override suspend fun List.toNotifications( imageLoader: ImageLoader, notificationAccountParams: NotificationAccountParams, ): List { - return messageEventToNotificationsResult(messages, imageLoader, notificationAccountParams) + return messageEventToNotificationsResult(this, imageLoader, notificationAccountParams) } @JvmName("toNotificationInvites") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( - invites: List, + override fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List { - return inviteToNotificationsResult(invites) + return inviteToNotificationsResult(this) } @JvmName("toNotificationSimpleEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( - simpleEvents: List, + override fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List { - return simpleEventToNotificationsResult(simpleEvents) + return simpleEventToNotificationsResult(this) } @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun toNotifications( - fallback: List, + override fun List.toNotifications( notificationAccountParams: NotificationAccountParams, ): List { - return fallbackEventToNotificationsResult(fallback) + return fallbackEventToNotificationsResult(this) } override fun createSummaryNotification( From 529c708d7eb04f1d17c21564b516b91f4271a283 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 26 Oct 2025 09:05:12 +0100 Subject: [PATCH 04/58] Rename OneShotNotification.key to OneShotNotification.tag for clarity. --- .../push/impl/notifications/NotificationDataFactory.kt | 8 ++++---- .../push/impl/notifications/NotificationRenderer.kt | 8 ++++---- .../impl/notifications/NotificationDataFactoryTest.kt | 4 ++-- .../push/impl/notifications/NotificationRendererTest.kt | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) 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 25cac5922d..ab8406f1af 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 @@ -114,7 +114,7 @@ class DefaultNotificationDataFactory( ): List { return map { event -> OneShotNotification( - key = event.roomId.value, + tag = event.roomId.value, notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event), summaryLine = event.description, isNoisy = event.noisy, @@ -130,7 +130,7 @@ class DefaultNotificationDataFactory( ): List { return map { event -> OneShotNotification( - key = event.eventId.value, + tag = event.eventId.value, notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event), summaryLine = event.description, isNoisy = event.noisy, @@ -146,7 +146,7 @@ class DefaultNotificationDataFactory( ): List { return map { event -> OneShotNotification( - key = event.eventId.value, + tag = event.eventId.value, notification = notificationCreator.createFallbackNotification(notificationAccountParams, event), summaryLine = event.description.orEmpty(), isNoisy = false, @@ -239,7 +239,7 @@ data class RoomNotification( data class OneShotNotification( val notification: Notification, - val key: String, + 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 4d6e588726..46018d48f4 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 @@ -94,9 +94,9 @@ class NotificationRenderer( invitationNotifications.forEach { notificationData -> if (useCompleteNotificationFormat) { - Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}") + Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.tag}") notificationDisplayer.showNotificationMessage( - tag = notificationData.key, + tag = notificationData.tag, id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId), notification = notificationData.notification ) @@ -105,9 +105,9 @@ class NotificationRenderer( simpleNotifications.forEach { notificationData -> if (useCompleteNotificationFormat) { - Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}") + Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.tag}") notificationDisplayer.showNotificationMessage( - tag = notificationData.key, + tag = notificationData.tag, id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId), notification = notificationData.notification ) 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 c8a7e6ba33..7bc19640e6 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 @@ -60,7 +60,7 @@ class NotificationDataFactoryTest { listOf( OneShotNotification( notification = expectedNotification, - key = A_ROOM_ID.value, + tag = A_ROOM_ID.value, summaryLine = AN_INVITATION_EVENT.description, isNoisy = AN_INVITATION_EVENT.noisy, timestamp = AN_INVITATION_EVENT.timestamp @@ -79,7 +79,7 @@ class NotificationDataFactoryTest { assertThat(result).containsExactly( OneShotNotification( notification = expectedNotification, - key = AN_EVENT_ID.value, + tag = AN_EVENT_ID.value, summaryLine = A_SIMPLE_EVENT.description, isNoisy = A_SIMPLE_EVENT.noisy, timestamp = AN_INVITATION_EVENT.timestamp 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 1026dca849..fa34552769 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 @@ -42,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, key = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1) + OneShotNotification(notification = A_NOTIFICATION, tag = "ignored", summaryLine = "ignored", isNoisy = false, timestamp = -1) @RunWith(RobolectricTestRunner::class) class NotificationRendererTest { @@ -86,7 +86,7 @@ class NotificationRendererTest { @Test fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest { - notificationCreator.createSimpleNotificationResult = lambdaRecorder { _, _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification } + notificationCreator.createSimpleNotificationResult = lambdaRecorder { _, _ -> ONE_SHOT_NOTIFICATION.copy(tag = AN_EVENT_ID.value).notification } renderEventsAsNotifications(listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID))) @@ -98,7 +98,7 @@ class NotificationRendererTest { @Test fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest { - notificationCreator.createRoomInvitationNotificationResult = lambdaRecorder { _, _ -> ONE_SHOT_NOTIFICATION.copy(key = AN_EVENT_ID.value).notification } + notificationCreator.createRoomInvitationNotificationResult = lambdaRecorder { _, _ -> ONE_SHOT_NOTIFICATION.copy(tag = AN_EVENT_ID.value).notification } renderEventsAsNotifications(listOf(anInviteNotifiableEvent())) From f0e42a70fb20e70e8f8c21bbf6967ec66fee32e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 26 Oct 2025 09:07:57 +0100 Subject: [PATCH 05/58] Use better names for API. --- .../impl/notifications/NotificationDisplayer.kt | 12 ++++++------ .../impl/notifications/NotificationRenderer.kt | 12 ++++++------ .../push/impl/push/OnRedactedEventReceived.kt | 2 +- .../impl/notifications/NotificationRendererTest.kt | 6 +++--- .../fake/FakeNotificationDisplayer.kt | 14 +++++++------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt index 4348c9bfb5..deb486547b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -19,8 +19,8 @@ import io.element.android.libraries.di.annotations.ApplicationContext import timber.log.Timber interface NotificationDisplayer { - fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean - fun cancelNotificationMessage(tag: String?, id: Int) + fun showNotification(tag: String?, id: Int, notification: Notification): Boolean + fun cancelNotification(tag: String?, id: Int) fun displayDiagnosticNotification(notification: Notification): Boolean fun dismissDiagnosticNotification() } @@ -30,7 +30,7 @@ class DefaultNotificationDisplayer( @ApplicationContext private val context: Context, private val notificationManager: NotificationManagerCompat ) : NotificationDisplayer { - override fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean { + override fun showNotification(tag: String?, id: Int, notification: Notification): Boolean { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { Timber.w("Not allowed to notify.") return false @@ -40,12 +40,12 @@ class DefaultNotificationDisplayer( return true } - override fun cancelNotificationMessage(tag: String?, id: Int) { + override fun cancelNotification(tag: String?, id: Int) { notificationManager.cancel(tag, id) } override fun displayDiagnosticNotification(notification: Notification): Boolean { - return showNotificationMessage( + return showNotification( tag = "DIAGNOSTIC", id = NOTIFICATION_ID_DIAGNOSTIC, notification = notification @@ -53,7 +53,7 @@ class DefaultNotificationDisplayer( } override fun dismissDiagnosticNotification() { - cancelNotificationMessage( + cancelNotification( tag = "DIAGNOSTIC", id = NOTIFICATION_ID_DIAGNOSTIC ) 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 46018d48f4..9f24ccecb0 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 @@ -74,7 +74,7 @@ class NotificationRenderer( // Remove summary first to avoid briefly displaying it after dismissing the last notification if (summaryNotification == SummaryNotification.Removed) { Timber.tag(loggerTag.value).d("Removing summary notification") - notificationDisplayer.cancelNotificationMessage( + notificationDisplayer.cancelNotification( tag = null, id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId) ) @@ -85,7 +85,7 @@ class NotificationRenderer( roomId = notificationData.roomId, threadId = notificationData.threadId ) - notificationDisplayer.showNotificationMessage( + notificationDisplayer.showNotification( tag = tag, id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId), notification = notificationData.notification @@ -95,7 +95,7 @@ class NotificationRenderer( invitationNotifications.forEach { notificationData -> if (useCompleteNotificationFormat) { Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.tag}") - notificationDisplayer.showNotificationMessage( + notificationDisplayer.showNotification( tag = notificationData.tag, id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId), notification = notificationData.notification @@ -106,7 +106,7 @@ class NotificationRenderer( simpleNotifications.forEach { notificationData -> if (useCompleteNotificationFormat) { Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.tag}") - notificationDisplayer.showNotificationMessage( + notificationDisplayer.showNotification( tag = notificationData.tag, id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId), notification = notificationData.notification @@ -117,7 +117,7 @@ class NotificationRenderer( // Show only the first fallback notification if (fallbackNotifications.isNotEmpty()) { Timber.tag(loggerTag.value).d("Showing fallback notification") - notificationDisplayer.showNotificationMessage( + notificationDisplayer.showNotification( tag = "FALLBACK", id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId), notification = fallbackNotifications.first().notification @@ -127,7 +127,7 @@ class NotificationRenderer( // Update summary last to avoid briefly displaying it before other notifications if (summaryNotification is SummaryNotification.Update) { Timber.tag(loggerTag.value).d("Updating summary notification") - notificationDisplayer.showNotificationMessage( + notificationDisplayer.showNotification( tag = null, id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId), notification = summaryNotification.notification diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt index 37d3b32c80..5be67606a4 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt @@ -73,7 +73,7 @@ class DefaultOnRedactedEventReceived( oldMessage.person ) messagingStyle.messages[messageToRedactIndex] = newMessage - notificationDisplayer.showNotificationMessage( + notificationDisplayer.showNotification( statusBarNotification.tag, statusBarNotification.id, NotificationCompat.Builder(context, notification) 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 fa34552769..cb97174cb1 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 @@ -78,7 +78,7 @@ class NotificationRendererTest { renderEventsAsNotifications(listOf(aNotifiableMessageEvent())) - notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence( + notificationDisplayer.showNotificationResult.assertions().isCalledExactly(2).withSequence( listOf(value(A_ROOM_ID.value), value(notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)), listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification)) ) @@ -90,7 +90,7 @@ class NotificationRendererTest { renderEventsAsNotifications(listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID))) - notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence( + notificationDisplayer.showNotificationResult.assertions().isCalledExactly(2).withSequence( listOf(value(AN_EVENT_ID.value), value(notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)), listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification)) ) @@ -102,7 +102,7 @@ class NotificationRendererTest { renderEventsAsNotifications(listOf(anInviteNotifiableEvent())) - notificationDisplayer.showNotificationMessageResult.assertions().isCalledExactly(2).withSequence( + notificationDisplayer.showNotificationResult.assertions().isCalledExactly(2).withSequence( listOf(value(A_ROOM_ID.value), value(notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID)), value(A_NOTIFICATION)), listOf(value(null), value(notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)), value(A_SUMMARY_NOTIFICATION.notification)) ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt index cd3d047e2e..d1c5de9ffb 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt @@ -19,17 +19,17 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value class FakeNotificationDisplayer( - var showNotificationMessageResult: LambdaThreeParamsRecorder = lambdaRecorder { _, _, _ -> true }, - var cancelNotificationMessageResult: LambdaTwoParamsRecorder = lambdaRecorder { _, _ -> }, + var showNotificationResult: LambdaThreeParamsRecorder = lambdaRecorder { _, _, _ -> true }, + var cancelNotificationResult: LambdaTwoParamsRecorder = lambdaRecorder { _, _ -> }, var displayDiagnosticNotificationResult: LambdaOneParamRecorder = lambdaRecorder { _ -> true }, var dismissDiagnosticNotificationResult: LambdaNoParamRecorder = lambdaRecorder { -> }, ) : NotificationDisplayer { - override fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean { - return showNotificationMessageResult(tag, id, notification) + override fun showNotification(tag: String?, id: Int, notification: Notification): Boolean { + return showNotificationResult(tag, id, notification) } - override fun cancelNotificationMessage(tag: String?, id: Int) { - return cancelNotificationMessageResult(tag, id) + override fun cancelNotification(tag: String?, id: Int) { + return cancelNotificationResult(tag, id) } override fun displayDiagnosticNotification(notification: Notification): Boolean { @@ -41,7 +41,7 @@ class FakeNotificationDisplayer( } fun verifySummaryCancelled(times: Int = 1) { - cancelNotificationMessageResult.assertions().isCalledExactly(times).withSequence( + cancelNotificationResult.assertions().isCalledExactly(times).withSequence( listOf(value(null), value(NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID))) ) } From 55517e5cb1e7c3bb376848d607159215d3b37e66 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 26 Oct 2025 09:09:10 +0100 Subject: [PATCH 06/58] Create const for diagnostic tag. --- .../push/impl/notifications/NotificationDisplayer.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt index deb486547b..decee9f198 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -46,7 +46,7 @@ class DefaultNotificationDisplayer( override fun displayDiagnosticNotification(notification: Notification): Boolean { return showNotification( - tag = "DIAGNOSTIC", + tag = TAG_DIAGNOSTIC, id = NOTIFICATION_ID_DIAGNOSTIC, notification = notification ) @@ -54,12 +54,13 @@ class DefaultNotificationDisplayer( override fun dismissDiagnosticNotification() { cancelNotification( - tag = "DIAGNOSTIC", + tag = TAG_DIAGNOSTIC, id = NOTIFICATION_ID_DIAGNOSTIC ) } companion object { + private const val TAG_DIAGNOSTIC = "DIAGNOSTIC" /* ========================================================================================== * IDs for notifications * ========================================================================================== */ From fbecf8d34f1cf08eab8f3667e186c4a8be008b4b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Oct 2025 08:26:12 +0100 Subject: [PATCH 07/58] Let DefaultNotificationDrawerManager use NotificationDisplayer instead of NotificationManagerCompat --- features/call/impl/build.gradle.kts | 1 + .../RingingCallNotificationCreatorTest.kt | 2 +- .../utils/DefaultActiveCallManagerTest.kt | 3 +- libraries/matrixui-test/build.gradle.kts | 20 ++++++++ .../matrix/ui/test/media/FakeImageLoader.kt | 50 +++++++++++++++++++ .../ui/test/media}/FakeImageLoaderHolder.kt | 11 ++-- libraries/push/impl/build.gradle.kts | 1 + .../DefaultNotificationDrawerManager.kt | 17 +++---- .../notifications/NotificationDisplayer.kt | 1 + .../DefaultBaseRoomGroupMessageCreatorTest.kt | 26 +++++----- .../DefaultNotificationDrawerManagerTest.kt | 31 ++++++------ ...aultOnMissedCallNotificationHandlerTest.kt | 9 ++-- .../NotificationDataFactoryTest.kt | 18 +++---- .../notifications/NotificationRendererTest.kt | 4 +- ...aultNotificationConversationServiceTest.kt | 2 +- .../DefaultNotificationCreatorTest.kt | 6 +-- .../test/notifications/FakeImageLoader.kt | 45 ----------------- 17 files changed, 136 insertions(+), 111 deletions(-) create mode 100644 libraries/matrixui-test/build.gradle.kts create mode 100644 libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoader.kt rename libraries/{push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications => matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media}/FakeImageLoaderHolder.kt (67%) delete mode 100644 libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index 9efe5ba75b..5c1b4ef0a6 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -93,6 +93,7 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.matrixuiTest) testImplementation(projects.libraries.push.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.appnavstate.test) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt index 0af482fc49..7ec5532e83 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider -import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.test.runTest diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index 3d1c35df4d..9464af603f 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -33,9 +33,9 @@ import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.NotificationIdProvider -import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.services.appnavstate.test.FakeAppForegroundStateService @@ -415,6 +415,7 @@ class DefaultActiveCallManagerTest { verify { notificationManagerCompat.cancel(any()) } } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun `IncomingCall - ignore expired ring lifetime`() = runTest { diff --git a/libraries/matrixui-test/build.gradle.kts b/libraries/matrixui-test/build.gradle.kts new file mode 100644 index 0000000000..c385d05b54 --- /dev/null +++ b/libraries/matrixui-test/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.matrix.ui.test" +} + +dependencies { + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(libs.coil.compose) +} diff --git a/libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoader.kt b/libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoader.kt new file mode 100644 index 0000000000..05e380fb9e --- /dev/null +++ b/libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoader.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.test.media + +import coil3.ComponentRegistry +import coil3.ImageLoader +import coil3.disk.DiskCache +import coil3.memory.MemoryCache +import coil3.request.Disposable +import coil3.request.ImageRequest +import coil3.request.ImageResult + +class FakeImageLoader : ImageLoader { + private val executedRequests = mutableListOf() + + override val defaults: ImageRequest.Defaults + get() = error("Not implemented") + override val components: ComponentRegistry + get() = error("Not implemented") + override val memoryCache: MemoryCache? + get() = error("Not implemented") + override val diskCache: DiskCache? + get() = error("Not implemented") + + override fun enqueue(request: ImageRequest): Disposable { + error("Not implemented") + } + + override suspend fun execute(request: ImageRequest): ImageResult { + executedRequests.add(request) + error("Not implemented") + } + + override fun shutdown() { + error("Not implemented") + } + + override fun newBuilder(): ImageLoader.Builder { + error("Not implemented") + } + + fun getExecutedRequestsData(): List { + return executedRequests.map { it.data } + } +} diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt b/libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt similarity index 67% rename from libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt rename to libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt index 4c92dc8c18..4deef7274b 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoaderHolder.kt +++ b/libraries/matrixui-test/src/main/kotlin/io/element/android/libraries/matrix/ui/test/media/FakeImageLoaderHolder.kt @@ -1,21 +1,22 @@ /* - * Copyright 2024 New Vector Ltd. + * Copyright 2025 New Vector Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.push.test.notifications +package io.element.android.libraries.matrix.ui.test.media import coil3.ImageLoader import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder -class FakeImageLoaderHolder : ImageLoaderHolder { - private val fakeImageLoader = FakeImageLoader() +class FakeImageLoaderHolder( + val fakeImageLoader: ImageLoader = FakeImageLoader(), +) : ImageLoaderHolder { override fun get(client: MatrixClient): ImageLoader { - return fakeImageLoader.getImageLoader() + return fakeImageLoader } override fun remove(sessionId: SessionId) { diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 87b3c681f1..2ea597b4f2 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { testCommonDependencies(libs) testImplementation(libs.coil.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.matrixuiTest) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.push.test) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 01a1b1f9a9..072ddcc891 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -8,7 +8,6 @@ package io.element.android.libraries.push.impl.notifications import androidx.annotation.VisibleForTesting -import androidx.core.app.NotificationManagerCompat import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn @@ -46,7 +45,7 @@ private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag. @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultNotificationDrawerManager( - private val notificationManager: NotificationManagerCompat, + private val notificationDisplayer: NotificationDisplayer, private val notificationRenderer: NotificationRenderer, private val appNavigationStateService: AppNavigationStateService, @AppCoroutineScope @@ -124,7 +123,7 @@ class DefaultNotificationDrawerManager( * Clear all known message events for a [sessionId]. */ override fun clearAllMessagesEvents(sessionId: SessionId) { - notificationManager.cancel(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) + notificationDisplayer.cancelNotification(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) clearSummaryNotificationIfNeeded(sessionId) } @@ -133,7 +132,7 @@ class DefaultNotificationDrawerManager( */ fun clearAllEvents(sessionId: SessionId) { activeNotificationsProvider.getNotificationsForSession(sessionId) - .forEach { notificationManager.cancel(it.tag, it.id) } + .forEach { notificationDisplayer.cancelNotification(it.tag, it.id) } } /** @@ -142,7 +141,7 @@ class DefaultNotificationDrawerManager( * Can also be called when a notification for this room is dismissed by the user. */ override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) { - notificationManager.cancel(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) + notificationDisplayer.cancelNotification(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) clearSummaryNotificationIfNeeded(sessionId) } @@ -158,7 +157,7 @@ class DefaultNotificationDrawerManager( override fun clearMembershipNotificationForSession(sessionId: SessionId) { activeNotificationsProvider.getMembershipNotificationForSession(sessionId) - .forEach { notificationManager.cancel(it.tag, it.id) } + .forEach { notificationDisplayer.cancelNotification(it.tag, it.id) } clearSummaryNotificationIfNeeded(sessionId) } @@ -167,7 +166,7 @@ class DefaultNotificationDrawerManager( */ override fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) { activeNotificationsProvider.getMembershipNotificationForRoom(sessionId, roomId) - .forEach { notificationManager.cancel(it.tag, it.id) } + .forEach { notificationDisplayer.cancelNotification(it.tag, it.id) } clearSummaryNotificationIfNeeded(sessionId) } @@ -176,14 +175,14 @@ class DefaultNotificationDrawerManager( */ override fun clearEvent(sessionId: SessionId, eventId: EventId) { val id = NotificationIdProvider.getRoomEventNotificationId(sessionId) - notificationManager.cancel(eventId.value, id) + notificationDisplayer.cancelNotification(eventId.value, id) clearSummaryNotificationIfNeeded(sessionId) } private fun clearSummaryNotificationIfNeeded(sessionId: SessionId) { val summaryNotification = activeNotificationsProvider.getSummaryNotification(sessionId) if (summaryNotification != null && activeNotificationsProvider.count(sessionId) == 1) { - notificationManager.cancel(null, summaryNotification.id) + notificationDisplayer.cancelNotification(null, summaryNotification.id) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt index decee9f198..c1e7ba1d29 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -61,6 +61,7 @@ class DefaultNotificationDisplayer( companion object { private const val TAG_DIAGNOSTIC = "DIAGNOSTIC" + /* ========================================================================================== * IDs for notifications * ========================================================================================== */ diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt index 00848390b5..f5acd8ddb4 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultBaseRoomGroupMessageCreatorTest.kt @@ -18,12 +18,12 @@ import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader import io.element.android.libraries.push.impl.notifications.factories.MARK_AS_READ_ACTION_TITLE import io.element.android.libraries.push.impl.notifications.factories.QUICK_REPLY_ACTION_TITLE import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent -import io.element.android.libraries.push.test.notifications.FakeImageLoader import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider @@ -51,7 +51,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { ) ), roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, existingNotification = null, threadId = null, ) @@ -59,7 +59,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { @Suppress("DEPRECATION") assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_LOW) assertThat(result.`when`).isEqualTo(A_TIMESTAMP) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty() } @Test @@ -74,13 +74,13 @@ class DefaultBaseRoomGroupMessageCreatorTest { ) ), roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, existingNotification = null, threadId = null, ) @Suppress("DEPRECATION") assertThat(result.priority).isEqualTo(NotificationCompat.PRIORITY_DEFAULT) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty() } @Test @@ -141,12 +141,12 @@ class DefaultBaseRoomGroupMessageCreatorTest { ) ), roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, existingNotification = null, threadId = null, ) assertThat(result.number).isEqualTo(1) - assertThat(fakeImageLoader.getCoilRequests()).containsExactlyElementsIn(expectedCoilRequests) + assertThat(fakeImageLoader.getExecutedRequestsData()).containsExactlyElementsIn(expectedCoilRequests) } @Test @@ -160,7 +160,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10), ), roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, existingNotification = null, threadId = null, ) @@ -173,7 +173,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { QUICK_REPLY_ACTION_TITLE.takeIf { NotificationConfig.SHOW_QUICK_REPLY_ACTION }, ) ) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty() } @Test @@ -189,7 +189,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { ), ), roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, existingNotification = null, threadId = null, ) @@ -199,7 +199,7 @@ class DefaultBaseRoomGroupMessageCreatorTest { MARK_AS_READ_ACTION_TITLE.takeIf { NotificationConfig.SHOW_MARK_AS_READ_ACTION } ) ) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty() } @Test @@ -214,13 +214,13 @@ class DefaultBaseRoomGroupMessageCreatorTest { ), ), roomId = A_ROOM_ID, - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, existingNotification = null, threadId = null, ) assertThat(result.number).isEqualTo(1) assertThat(result.`when`).isEqualTo(A_TIMESTAMP) - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty() } } 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 275c1ac490..b3c4569470 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 @@ -9,7 +9,6 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import androidx.compose.ui.graphics.Color -import androidx.core.app.NotificationManagerCompat import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService @@ -22,14 +21,15 @@ import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator +import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer 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.aNotifiableMessageEvent -import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.services.appnavstate.api.AppNavigationState @@ -43,19 +43,14 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.mockk.every import io.mockk.mockk -import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment @OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) class DefaultNotificationDrawerManagerTest { @Test fun `clearAllEvents should have no effect when queue is empty`() = runTest { @@ -183,10 +178,12 @@ class DefaultNotificationDrawerManagerTest { @Test fun `clearSummaryNotificationIfNeeded will run after clearing all other notifications`() = runTest { - val notificationManager = mockk { - every { cancel(any(), any()) } returns Unit - } + val cancelNotificationResult = lambdaRecorder { _, _ -> } + val notificationDisplayer = FakeNotificationDisplayer( + cancelNotificationResult = cancelNotificationResult, + ) val summaryId = NotificationIdProvider.getSummaryNotificationId(A_SESSION_ID) + val roomMessageId = NotificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID) val activeNotificationsProvider = FakeActiveNotificationsProvider( getSummaryNotificationResult = { mockk { @@ -196,7 +193,7 @@ class DefaultNotificationDrawerManagerTest { countResult = { 1 }, ) val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( - notificationManager = notificationManager, + notificationDisplayer = notificationDisplayer, activeNotificationsProvider = activeNotificationsProvider, ) @@ -204,13 +201,16 @@ class DefaultNotificationDrawerManagerTest { defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID) // Verify we asked to cancel the notification with summaryId - verify { notificationManager.cancel(null, summaryId) } + cancelNotificationResult.assertions().isCalledExactly(2).withSequence( + listOf(value(null), value(roomMessageId)), + listOf(value(null), value(summaryId)), + ) defaultNotificationDrawerManager.destroy() } private fun TestScope.createDefaultNotificationDrawerManager( - notificationManager: NotificationManagerCompat = NotificationManagerCompat.from(RuntimeEnvironment.getApplication()), + notificationDisplayer: NotificationDisplayer = FakeNotificationDisplayer(), appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(), roomGroupMessageCreator: RoomGroupMessageCreator = FakeRoomGroupMessageCreator(), summaryGroupMessageCreator: SummaryGroupMessageCreator = FakeSummaryGroupMessageCreator(), @@ -219,11 +219,10 @@ class DefaultNotificationDrawerManagerTest { sessionStore: SessionStore = InMemorySessionStore(), enterpriseService: EnterpriseService = FakeEnterpriseService(), ): DefaultNotificationDrawerManager { - val context = RuntimeEnvironment.getApplication() return DefaultNotificationDrawerManager( - notificationManager = notificationManager, + notificationDisplayer = notificationDisplayer, notificationRenderer = NotificationRenderer( - notificationDisplayer = DefaultNotificationDisplayer(context, NotificationManagerCompat.from(context)), + notificationDisplayer = FakeNotificationDisplayer(), notificationDataFactory = DefaultNotificationDataFactory( notificationCreator = FakeNotificationCreator(), roomGroupMessageCreator = roomGroupMessageCreator, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt index e5a4b01043..f2b26b3437 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt @@ -15,22 +15,19 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notification.aNotificationData +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDataFactory +import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver -import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -@RunWith(RobolectricTestRunner::class) class DefaultOnMissedCallNotificationHandlerTest { @OptIn(ExperimentalCoroutinesApi::class) @Test @@ -50,7 +47,7 @@ class DefaultOnMissedCallNotificationHandlerTest { val defaultOnMissedCallNotificationHandler = DefaultOnMissedCallNotificationHandler( matrixClientProvider = matrixClientProvider, defaultNotificationDrawerManager = DefaultNotificationDrawerManager( - notificationManager = mockk(relaxed = true), + notificationDisplayer = FakeNotificationDisplayer(), notificationRenderer = createNotificationRenderer( notificationDataFactory = dataFactory, ), 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 7bc19640e6..50e0b5dfb8 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 @@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator @@ -21,7 +22,6 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGrou 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.libraries.push.test.notifications.FakeImageLoader import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.test.runTest import org.junit.Test @@ -98,7 +98,7 @@ class NotificationDataFactoryTest { events = events, roomId = A_ROOM_ID, threadId = null, - imageLoader = FakeImageLoader().getImageLoader(), + imageLoader = FakeImageLoader(), existingNotification = null, ), roomId = A_ROOM_ID, @@ -113,12 +113,12 @@ class NotificationDataFactoryTest { notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, ) assertThat(result.size).isEqualTo(1) assertThat(result.first().isDataEqualTo(expectedNotification)).isTrue() - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty() } @Test @@ -129,10 +129,10 @@ class NotificationDataFactoryTest { notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, ) assertThat(result).isEmpty() - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty() } @Test @@ -152,7 +152,7 @@ class NotificationDataFactoryTest { events = withRedactedRemoved, roomId = A_ROOM_ID, threadId = null, - imageLoader = FakeImageLoader().getImageLoader(), + imageLoader = FakeImageLoader(), existingNotification = null, ), roomId = A_ROOM_ID, @@ -168,12 +168,12 @@ class NotificationDataFactoryTest { notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), - imageLoader = fakeImageLoader.getImageLoader(), + imageLoader = fakeImageLoader, ) assertThat(result.size).isEqualTo(1) assertThat(result.first().isDataEqualTo(expectedNotification)).isTrue() - assertThat(fakeImageLoader.getCoilRequests().size).isEqualTo(0) + assertThat(fakeImageLoader.getExecutedRequestsData()).isEmpty() } } 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 cb97174cb1..00ce37b09b 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 @@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator @@ -25,7 +26,6 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent -import io.element.android.libraries.push.test.notifications.FakeImageLoader 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 @@ -113,7 +113,7 @@ class NotificationRendererTest { MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL), useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT, eventsToProcess = events, - imageLoader = FakeImageLoader().getImageLoader(), + imageLoader = FakeImageLoader(), ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt index d05966f9a6..82441883a7 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/conversations/DefaultNotificationConversationServiceTest.kt @@ -20,9 +20,9 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoaderHolder import io.element.android.libraries.push.impl.notifications.factories.FakeIntentProvider import io.element.android.libraries.push.impl.notifications.shortcut.createShortcutId -import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver import kotlinx.coroutines.ExperimentalCoroutinesApi 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 df9f106972..b95b3128fc 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 @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.test.media.FakeImageLoader import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.impl.notifications.DefaultNotificationBitmapLoader import io.element.android.libraries.push.impl.notifications.NotificationActionIds @@ -36,7 +37,6 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable 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.libraries.push.test.notifications.FakeImageLoader 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.A_FAKE_TIMESTAMP @@ -246,7 +246,7 @@ class DefaultNotificationCreatorTest { lastMessageTimestamp = 123_456L, tickerText = "tickerText", existingNotification = null, - imageLoader = FakeImageLoader().getImageLoader(), + imageLoader = FakeImageLoader(), events = listOf(aNotifiableMessageEvent()), ) result.commonAssertions() @@ -271,7 +271,7 @@ class DefaultNotificationCreatorTest { lastMessageTimestamp = 123_456L, tickerText = "tickerText", existingNotification = null, - imageLoader = FakeImageLoader().getImageLoader(), + imageLoader = FakeImageLoader(), events = listOf(aNotifiableMessageEvent()), ) result.commonAssertions() diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt deleted file mode 100644 index 067376cfbc..0000000000 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/notifications/FakeImageLoader.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.push.test.notifications - -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import coil3.ImageLoader -import coil3.test.FakeImageLoaderEngine -import coil3.test.intercept -import org.robolectric.RuntimeEnvironment - -class FakeImageLoader { - private val coilRequests = mutableListOf() - - private var cache: ImageLoader? = null - - fun getImageLoader(): ImageLoader { - return cache ?: ImageLoader.Builder(RuntimeEnvironment.getApplication()) - .components { - val engine = FakeImageLoaderEngine.Builder() - .intercept( - predicate = { - coilRequests.add(it) - true - }, - drawable = ColorDrawable(Color.BLUE) - ) - .build() - add(engine) - } - .build() - .also { - cache = it - } - } - - fun getCoilRequests(): List { - return coilRequests.toList() - } -} From 31ff72b319b7cce2e42e3fc44fd7755b188a108d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Oct 2025 10:58:28 +0100 Subject: [PATCH 08/58] Use TestScope.backgroundScope --- .../DefaultNotificationDrawerManager.kt | 12 +----------- .../DefaultNotificationDrawerManagerTest.kt | 11 ++--------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 072ddcc891..71e5f8b7af 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.push.impl.notifications -import androidx.annotation.VisibleForTesting import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn @@ -31,7 +30,6 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.currentSessionId import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import timber.log.Timber @@ -54,25 +52,17 @@ class DefaultNotificationDrawerManager( private val imageLoaderHolder: ImageLoaderHolder, private val activeNotificationsProvider: ActiveNotificationsProvider, ) : NotificationCleaner { - private var appNavigationStateObserver: Job? = null - // TODO EAx add a setting per user for this private var useCompleteNotificationFormat = true init { // Observe application state - appNavigationStateObserver = coroutineScope.launch { + coroutineScope.launch { appNavigationStateService.appNavigationState .collect { onAppNavigationStateChange(it.navigationState) } } } - // For test only - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun destroy() { - appNavigationStateObserver?.cancel() - } - private var currentAppNavigationState: NavigationState? = null private fun onAppNavigationStateChange(navigationState: NavigationState) { 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 b3c4569470..ff484df96f 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 @@ -56,7 +56,6 @@ class DefaultNotificationDrawerManagerTest { fun `clearAllEvents should have no effect when queue is empty`() = runTest { val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() defaultNotificationDrawerManager.clearAllEvents(A_SESSION_ID) - defaultNotificationDrawerManager.destroy() } @Test @@ -88,7 +87,6 @@ class DefaultNotificationDrawerManagerTest { defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) // Add the same Event again (will be ignored) defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) - defaultNotificationDrawerManager.destroy() } @Test @@ -101,7 +99,7 @@ class DefaultNotificationDrawerManagerTest { ) ) val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow) - val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( + createDefaultNotificationDrawerManager( appNavigationStateService = appNavigationStateService ) appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) @@ -117,7 +115,6 @@ class DefaultNotificationDrawerManagerTest { // Like a user sign out appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) runCurrent() - defaultNotificationDrawerManager.destroy() } @Test @@ -172,8 +169,6 @@ class DefaultNotificationDrawerManagerTest { any(), ), ) - - defaultNotificationDrawerManager.destroy() } @Test @@ -205,8 +200,6 @@ class DefaultNotificationDrawerManagerTest { listOf(value(null), value(roomMessageId)), listOf(value(null), value(summaryId)), ) - - defaultNotificationDrawerManager.destroy() } private fun TestScope.createDefaultNotificationDrawerManager( @@ -234,7 +227,7 @@ class DefaultNotificationDrawerManagerTest { sessionStore = sessionStore, ), appNavigationStateService = appNavigationStateService, - coroutineScope = this, + coroutineScope = backgroundScope, matrixClientProvider = matrixClientProvider, imageLoaderHolder = FakeImageLoaderHolder(), activeNotificationsProvider = activeNotificationsProvider, From 947347b3f178d949d9f1c264a1cdca3a7adffb76 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Oct 2025 12:32:36 +0100 Subject: [PATCH 09/58] Move call to `setGroup` to `configureWith` --- .../impl/notifications/factories/NotificationCreator.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 411cb25db1..79b4bb7db0 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 @@ -155,9 +155,6 @@ class DefaultNotificationCreator( setShortcutId(createShortcutId(roomInfo.sessionId, roomInfo.roomId)) } } - // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) - // devices and all Wear devices. But we want a custom grouping, so we specify the groupID - .setGroup(roomInfo.sessionId.value) .setGroupSummary(false) // In order to avoid notification making sound twice (due to the summary notification) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) @@ -236,7 +233,6 @@ class DefaultNotificationCreator( .setOnlyAlertOnce(true) .setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5)) .setContentText(inviteNotifiableEvent.description.annotateForDebug(6)) - .setGroup(inviteNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .configureWith(notificationAccountParams) .apply { @@ -277,7 +273,6 @@ class DefaultNotificationCreator( .setOnlyAlertOnce(true) .setContentTitle(buildMeta.applicationName.annotateForDebug(7)) .setContentText(simpleNotifiableEvent.description.annotateForDebug(8)) - .setGroup(simpleNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .configureWith(notificationAccountParams) .setAutoCancel(true) @@ -308,7 +303,6 @@ class DefaultNotificationCreator( .setOnlyAlertOnce(true) .setContentTitle(buildMeta.applicationName.annotateForDebug(7)) .setContentText(fallbackNotifiableEvent.description.orEmpty().annotateForDebug(8)) - .setGroup(fallbackNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .configureWith(notificationAccountParams) .setAutoCancel(true) @@ -343,7 +337,6 @@ class DefaultNotificationCreator( // used in compat < N, after summary is built based on child notifications .setWhen(lastMessageTimestamp) .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setGroup(userId.value) // set this notification as the summary for the group .setGroupSummary(true) .configureWith(notificationAccountParams) @@ -476,6 +469,7 @@ class DefaultNotificationCreator( private fun NotificationCompat.Builder.configureWith(notificationAccountParams: NotificationAccountParams) = apply { setSmallIcon(CommonDrawables.ic_notification) setColor(notificationAccountParams.color) + setGroup(notificationAccountParams.user.userId.value) if (notificationAccountParams.showSessionId) { setSubText(notificationAccountParams.user.userId.value) } From d5fdc5f8e6c3e3b96bfde4cf4b505dfec83fe284 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Oct 2025 12:40:59 +0100 Subject: [PATCH 10/58] Small cleanup --- .../push/impl/notifications/factories/NotificationCreator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 79b4bb7db0..390f6b66d7 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 @@ -164,7 +164,7 @@ class DefaultNotificationCreator( val messagingStyle = existingNotification?.let { MessagingStyle.extractMessagingStyleFromNotification(it) - } ?: messagingStyleFromCurrentUser( + } ?: createMessagingStyleFromCurrentUser( user = notificationAccountParams.user, imageLoader = imageLoader, roomName = roomInfo.roomDisplayName, @@ -437,7 +437,7 @@ class DefaultNotificationCreator( } } - private suspend fun messagingStyleFromCurrentUser( + private suspend fun createMessagingStyleFromCurrentUser( user: MatrixUser, imageLoader: ImageLoader, roomName: String, From 231077d94b6a695a06de8254dc0d961365108342 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 27 Oct 2025 12:50:12 +0100 Subject: [PATCH 11/58] Remove obsolete TODO --- .../push/impl/notifications/SummaryGroupMessageCreator.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 8600472c9d..80bc92dc23 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 @@ -49,13 +49,10 @@ class DefaultSummaryGroupMessageCreator( val summaryIsNoisy = roomNotifications.any { it.shouldBing } || invitationNotifications.any { it.isNoisy } || simpleNotifications.any { it.isNoisy } - val lastMessageTimestamp = roomNotifications.lastOrNull()?.latestTimestamp ?: invitationNotifications.lastOrNull()?.timestamp ?: simpleNotifications.last().timestamp - - // FIXME roomIdToEventMap.size is not correct, this is the number of rooms - val nbEvents = roomNotifications.size + simpleNotifications.size + val nbEvents = roomNotifications.size + invitationNotifications.size + simpleNotifications.size val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) return notificationCreator.createSummaryListNotification( notificationAccountParams = notificationAccountParams, From af2429d8edc1198fbec64580d335e8d973ba121c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Oct 2025 09:42:22 +0100 Subject: [PATCH 12/58] Fix issue after rebase. --- .../push/impl/notifications/DefaultNotificationDrawerManager.kt | 2 +- .../push/impl/push/DefaultOnRedactedEventReceivedTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 71e5f8b7af..f3ba1c6d84 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -141,7 +141,7 @@ class DefaultNotificationDrawerManager( */ override fun clearMessagesForThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) { val tag = NotificationCreator.messageTag(roomId, threadId) - notificationManager.cancel(tag, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) + notificationDisplayer.cancelNotification(tag, NotificationIdProvider.getRoomMessagesNotificationId(sessionId)) clearSummaryNotificationIfNeeded(sessionId) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt index b27c96d8de..71ec7c3545 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultOnRedactedEventReceivedTest.kt @@ -122,7 +122,7 @@ class DefaultOnRedactedEventReceivedTest { } ) }, - displayer = FakeNotificationDisplayer(showNotificationMessageResult = showNotificationLambda), + displayer = FakeNotificationDisplayer(showNotificationResult = showNotificationLambda), ) sut.onRedactedEventsReceived(listOf(ResolvedPushEvent.Redaction(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, null))) From df4853e1f5deb4bfbdf1b9b5c302cbbe5d0bef8e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Oct 2025 10:10:00 +0100 Subject: [PATCH 13/58] Fix test. --- .../notifications/factories/DefaultNotificationCreatorTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b95b3128fc..419a348f08 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 @@ -278,7 +278,7 @@ class DefaultNotificationCreatorTest { } private fun Notification.commonAssertions( - expectedGroup: String? = A_SESSION_ID.value, + expectedGroup: String? = aMatrixUser().userId.value, expectedCategory: String? = NotificationCompat.CATEGORY_MESSAGE, ) { assertThat(contentIntent).isNotNull() From aae384bba898967d40952ad35cb9e06c60f04b0c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:45:27 +0000 Subject: [PATCH 14/58] fix(deps): update dependency io.element.android:element-call-embedded to v0.16.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 08ec71915c..54bc1660f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -229,7 +229,7 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0" metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } # Element Call -element_call_embedded = "io.element.android:element-call-embedded:0.16.0" +element_call_embedded = "io.element.android:element-call-embedded:0.16.1" # Auto services google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } From c4b8227c58ea1ff1269eaefb9560f7007c33c2e3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 30 Oct 2025 22:15:08 +0100 Subject: [PATCH 15/58] feature(space) : starts space settings screen --- .../impl/settings/SpaceSettingsEvents.kt | 10 + .../space/impl/settings/SpaceSettingsNode.kt | 58 +++++ .../impl/settings/SpaceSettingsPresenter.kt | 38 ++++ .../space/impl/settings/SpaceSettingsState.kt | 23 ++ .../settings/SpaceSettingsStateProvider.kt | 44 ++++ .../space/impl/settings/SpaceSettingsView.kt | 215 ++++++++++++++++++ 6 files changed, 388 insertions(+) create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvents.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPresenter.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt create mode 100644 features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvents.kt new file mode 100644 index 0000000000..a6fe90ade6 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvents.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.settings + +sealed interface SpaceSettingsEvents diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt new file mode 100644 index 0000000000..77ae924f94 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.settings + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.annotations.ContributesNode +import io.element.android.features.space.impl.di.SpaceFlowScope +import io.element.android.libraries.architecture.appyx.launchMolecule + +@ContributesNode(SpaceFlowScope::class) +@AssistedInject +class SpaceSettingsNode( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: SpaceSettingsPresenter, +) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onBackClick() + + fun onSpaceInfoClick() + fun onMembersClick() + fun onRolesAndPermissionsClick() + fun onSecurityAndPrivacyClick() + fun onLeaveSpaceClick() + } + + private val callback = plugins().single() + private val stateFlow = launchMolecule { presenter.present() } + + @Composable + override fun View(modifier: Modifier) { + val state by stateFlow.collectAsState() + SpaceSettingsView( + state = state, + modifier = modifier, + onSpaceInfoClick = callback::onSpaceInfoClick, + onBackClick = callback::onBackClick, + onMembersClick = callback::onMembersClick, + onRolesAndPermissionsClick = callback::onRolesAndPermissionsClick, + onSecurityAndPrivacyClick = callback::onSecurityAndPrivacyClick, + onLeaveSpaceClick = callback::onLeaveSpaceClick, + ) + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPresenter.kt new file mode 100644 index 0000000000..48cb9f0b3b --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPresenter.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.settings + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import dev.zacsweers.metro.Inject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin + +@Inject +class SpaceSettingsPresenter( + private val room: JoinedRoom, +) : Presenter { + @Composable + override fun present(): SpaceSettingsState { + val roomInfo by room.roomInfoFlow.collectAsState() + val isUserAdmin = room.isOwnUserAdmin() + return SpaceSettingsState( + roomId = room.roomId, + name = roomInfo.name.orEmpty(), + canonicalAlias = roomInfo.canonicalAlias, + avatarUrl = roomInfo.avatarUrl, + memberCount = roomInfo.activeMembersCount, + showRolesAndPermissions = isUserAdmin, + showSecurityAndPrivacy = isUserAdmin, + isTombstoned = roomInfo.successorRoom != null, + eventSink = {}, + ) + } +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt new file mode 100644 index 0000000000..b3d3353f06 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.settings + +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId + +data class SpaceSettingsState( + val roomId: RoomId, + val name: String, + val canonicalAlias: RoomAlias?, + val avatarUrl: String?, + val isTombstoned: Boolean, + val memberCount: Long, + val showRolesAndPermissions: Boolean, + val showSecurityAndPrivacy: Boolean, + val eventSink: (SpaceSettingsEvents) -> Unit +) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt new file mode 100644 index 0000000000..e248c1798a --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.settings + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId + +open class SpaceSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSpaceSettingsState(), + aSpaceSettingsState(alias = null), + aSpaceSettingsState(showSecurityAndPrivacy = true), + aSpaceSettingsState(showRolesAndPermissions = true), + ) +} + +fun aSpaceSettingsState( + roomId: RoomId = RoomId("!aRoomId:element.io"), + name: String = "Space name", + alias: RoomAlias? = RoomAlias("#spacename:element.io"), + avatarUrl: String? = null, + memberCount: Long = 100, + isTombstoned: Boolean = false, + showRolesAndPermissions: Boolean = false, + showSecurityAndPrivacy: Boolean = false, + eventSink: (SpaceSettingsEvents) -> Unit = {}, +) = SpaceSettingsState( + roomId = roomId, + name = name, + canonicalAlias = alias, + avatarUrl = avatarUrl, + isTombstoned = isTombstoned, + memberCount = memberCount, + showRolesAndPermissions = showRolesAndPermissions, + showSecurityAndPrivacy = showSecurityAndPrivacy, + eventSink = eventSink, +) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt new file mode 100644 index 0000000000..eaefcf59d5 --- /dev/null +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.space.impl.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.avatar.AvatarType +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun SpaceSettingsView( + state: SpaceSettingsState, + onBackClick: () -> Unit, + onSpaceInfoClick: ()->Unit, + onMembersClick: () -> Unit, + onRolesAndPermissionsClick: () -> Unit, + onSecurityAndPrivacyClick: () -> Unit, + onLeaveSpaceClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + SpaceSettingsTopBar(onBackClick = onBackClick) + }, + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onSpaceInfoClick) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + avatarData = AvatarData(state.roomId.value, state.name, state.avatarUrl, AvatarSize.SpaceListItem), + avatarType = AvatarType.Space( + isTombstoned = state.isTombstoned, + ), + contentDescription = state.avatarUrl?.let { stringResource(CommonStrings.a11y_room_avatar) }, + ) + Spacer(Modifier.width(16.dp)) + Column { + Text( + text = state.name, + style = ElementTheme.typography.fontHeadingMdRegular, + color = ElementTheme.colors.textPrimary, + ) + if (state.canonicalAlias != null) { + Text( + text = state.canonicalAlias.value, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) + } + } + } + Section(isVisible = state.showSecurityAndPrivacy) { + SecurityAndPrivacyItem( + onClick = onSecurityAndPrivacyClick + ) + } + Section { + MembersItem(state.memberCount, onClick = onMembersClick) + if (state.showRolesAndPermissions) { + RolesAndPermissionsItem(onClick = onRolesAndPermissionsClick) + } + } + Section { + LeaveSpaceItem( + onClick = onLeaveSpaceClick + ) + } + + } + } +} + +@Composable +private fun ColumnScope.Section( + modifier: Modifier = Modifier, + isVisible: Boolean = true, + content: @Composable ColumnScope.() -> Unit, +) { + if (isVisible) { + PreferenceCategory(content = content, modifier = modifier) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SpaceSettingsTopBar( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + titleStr = stringResource(CommonStrings.common_settings), + navigationIcon = { BackButton(onClick = onBackClick) }, + modifier = modifier, + ) +} + +@Composable +private fun SecurityAndPrivacyItem( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ListItem( + headlineContent = { Text("Security & privacy") }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), + onClick = onClick, + modifier = modifier, + ) +} + +@Composable +private fun MembersItem( + memberCount: Long, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ListItem( + headlineContent = { Text(stringResource(CommonStrings.common_people)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.User())), + trailingContent = ListItemContent.Text(memberCount.toString()), + onClick = onClick, + modifier = modifier, + ) +} + +@Composable +private fun RolesAndPermissionsItem( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ListItem( + headlineContent = { Text("Roles & permissions") }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())), + onClick = onClick, + modifier = modifier, + ) +} + +@Composable +private fun LeaveSpaceItem( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ListItem( + headlineContent = { + Text(stringResource(CommonStrings.action_leave_space)) + }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Leave())), + style = ListItemStyle.Destructive, + onClick = onClick, + modifier = modifier, + ) +} + +@PreviewsDayNight +@Composable +internal fun SpaceSettingsViewPreview( + @PreviewParameter(SpaceSettingsStateProvider::class) state: SpaceSettingsState +) = ElementPreview { + SpaceSettingsView( + state = state, + onBackClick = {}, + onSpaceInfoClick = {}, + onMembersClick = {}, + onRolesAndPermissionsClick = {}, + onSecurityAndPrivacyClick = {}, + onLeaveSpaceClick = {}, + modifier = Modifier, + ) +} From 9d3b7de0d8ff8b7a54a472a0c22d5cd45a2b65a2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 30 Oct 2025 22:15:30 +0100 Subject: [PATCH 16/58] feature(space) : remove dead code # Conflicts: # appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt --- .../io/element/android/appnav/room/RoomFlowNode.kt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index a41eb8d777..3189384f53 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -30,7 +30,6 @@ import io.element.android.features.joinroom.api.JoinRoomEntryPoint import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint.Params import io.element.android.features.roomdirectory.api.RoomDescription -import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -70,7 +69,6 @@ class RoomFlowNode( private val joinRoomEntryPoint: JoinRoomEntryPoint, private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint, private val membershipObserver: RoomMembershipObserver, - private val spaceEntryPoint: SpaceEntryPoint, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Loading, @@ -105,9 +103,6 @@ class RoomFlowNode( @Parcelize data class JoinedRoom(val roomId: RoomId) : NavTarget - - @Parcelize - data class JoinedSpace(val spaceId: RoomId) : NavTarget } override fun onBuilt() { @@ -209,15 +204,6 @@ class RoomFlowNode( ) createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) } - is NavTarget.JoinedSpace -> { - val spaceCallback = plugins().single() - spaceEntryPoint.createNode( - parentNode = this, - buildContext = buildContext, - inputs = SpaceEntryPoint.Inputs(roomId = navTarget.spaceId), - callback = spaceCallback, - ) - } } } From 5f7fda240279894de3a692778f576d0af223004f Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 30 Oct 2025 22:16:20 +0100 Subject: [PATCH 17/58] feature(space) : plumb up space settings screen --- .../room/joined/JoinedRoomLoadedFlowNode.kt | 4 -- .../features/space/api/SpaceEntryPoint.kt | 1 - .../features/space/impl/SpaceFlowNode.kt | 47 ++++++++++++++++--- .../space/impl/leave/LeaveSpaceNode.kt | 7 ++- .../features/space/impl/root/SpaceNode.kt | 4 +- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 16eaff89b1..d6e3e13e89 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -195,10 +195,6 @@ class JoinedRoomLoadedFlowNode( callback.navigateToRoom(roomId, viaParameters) } - override fun navigateToRoomDetails() { - backstack.push(NavTarget.RoomDetails) - } - override fun navigateToRoomMemberList() { backstack.push(NavTarget.RoomMemberList) } diff --git a/features/space/api/src/main/kotlin/io/element/android/features/space/api/SpaceEntryPoint.kt b/features/space/api/src/main/kotlin/io/element/android/features/space/api/SpaceEntryPoint.kt index 6b5bd7f892..e05a7d1e8b 100644 --- a/features/space/api/src/main/kotlin/io/element/android/features/space/api/SpaceEntryPoint.kt +++ b/features/space/api/src/main/kotlin/io/element/android/features/space/api/SpaceEntryPoint.kt @@ -28,7 +28,6 @@ interface SpaceEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun navigateToRoom(roomId: RoomId, viaParameters: List) - fun navigateToRoomDetails() fun navigateToRoomMemberList() } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt index 1ef496d319..6684d07952 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -18,6 +18,7 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject @@ -26,14 +27,15 @@ import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.features.space.impl.di.SpaceFlowGraph import io.element.android.features.space.impl.leave.LeaveSpaceNode import io.element.android.features.space.impl.root.SpaceNode +import io.element.android.features.space.impl.settings.SpaceSettingsNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.callback import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.spaces.SpaceService import kotlinx.parcelize.Parcelize @@ -42,6 +44,7 @@ import kotlinx.parcelize.Parcelize class SpaceFlowNode( @Assisted val buildContext: BuildContext, @Assisted plugins: List, + room: BaseRoom, spaceService: SpaceService, graphFactory: SpaceFlowGraph.Factory, ) : BaseFlowNode( @@ -52,15 +55,17 @@ class SpaceFlowNode( buildContext = buildContext, plugins = plugins, ), DependencyInjectionGraphOwner { - private val inputs: SpaceEntryPoint.Inputs = inputs() private val callback: SpaceEntryPoint.Callback = callback() - private val spaceRoomList = spaceService.spaceRoomList(inputs.roomId) + private val spaceRoomList = spaceService.spaceRoomList(room.roomId) override val graph = graphFactory.create(spaceRoomList) sealed interface NavTarget : Parcelable { @Parcelize data object Root : NavTarget + @Parcelize + data object Settings : NavTarget + @Parcelize data object Leave : NavTarget } @@ -77,7 +82,7 @@ class SpaceFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Leave -> { - createNode(buildContext, listOf(inputs)) + createNode(buildContext) } NavTarget.Root -> { val callback = object : SpaceNode.Callback { @@ -85,8 +90,8 @@ class SpaceFlowNode( callback.navigateToRoom(roomId, viaParameters) } - override fun navigateToRoomDetails() { - callback.navigateToRoomDetails() + override fun navigateToSpaceSettings() { + backstack.push(NavTarget.Settings) } override fun navigateToRoomMemberList() { @@ -97,7 +102,35 @@ class SpaceFlowNode( backstack.push(NavTarget.Leave) } } - createNode(buildContext, listOf(inputs, callback)) + createNode(buildContext, listOf(callback)) + } + NavTarget.Settings -> { + val callback = object : SpaceSettingsNode.Callback { + override fun onBackClick() { + backstack.pop() + } + + override fun onSpaceInfoClick() { + //TODO + } + + override fun onMembersClick() { + callback.navigateToRoomMemberList() + } + + override fun onRolesAndPermissionsClick() { + //TODO + } + + override fun onSecurityAndPrivacyClick() { + //TODO + } + + override fun onLeaveSpaceClick() { + backstack.push(NavTarget.Leave) + } + } + createNode(buildContext, listOf(callback)) } } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt index c60bddea1d..215f06bca5 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt @@ -16,10 +16,9 @@ import com.bumble.appyx.core.plugin.Plugin import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode -import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.features.space.impl.di.SpaceFlowScope -import io.element.android.libraries.architecture.inputs import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.JoinedRoom @ContributesNode(SpaceFlowScope::class) @AssistedInject @@ -27,10 +26,10 @@ class LeaveSpaceNode( @Assisted buildContext: BuildContext, @Assisted plugins: List, matrixClient: MatrixClient, + room: JoinedRoom, presenterFactory: LeaveSpacePresenter.Factory, ) : Node(buildContext, plugins = plugins) { - private val inputs: SpaceEntryPoint.Inputs = inputs() - private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(inputs.roomId) + private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId) private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle) override fun onBuilt() { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt index 174fa71ee8..c0271782b4 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceNode.kt @@ -42,7 +42,7 @@ class SpaceNode( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun navigateToRoom(roomId: RoomId, viaParameters: List) - fun navigateToRoomDetails() + fun navigateToSpaceSettings() fun navigateToRoomMemberList() fun startLeaveSpaceFlow() } @@ -80,7 +80,7 @@ class SpaceNode( callback.navigateToRoom(spaceRoom.roomId, spaceRoom.via) }, onDetailsClick = { - callback.navigateToRoomDetails() + callback.navigateToSpaceSettings() }, onShareSpace = { onShareRoom(context) From 4c9b6100a76f723f743114d34f76d5a2ffb55c20 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 31 Oct 2025 14:33:11 +0100 Subject: [PATCH 18/58] feature(space) : iterate on SpaceSettings --- .../impl/settings/SpaceSettingsPresenter.kt | 1 - .../space/impl/settings/SpaceSettingsState.kt | 1 - .../settings/SpaceSettingsStateProvider.kt | 2 - .../space/impl/settings/SpaceSettingsView.kt | 92 +++++++++++-------- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPresenter.kt index 48cb9f0b3b..6e89a0ba8d 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsPresenter.kt @@ -31,7 +31,6 @@ class SpaceSettingsPresenter( memberCount = roomInfo.activeMembersCount, showRolesAndPermissions = isUserAdmin, showSecurityAndPrivacy = isUserAdmin, - isTombstoned = roomInfo.successorRoom != null, eventSink = {}, ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt index b3d3353f06..95b3615f63 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt @@ -15,7 +15,6 @@ data class SpaceSettingsState( val name: String, val canonicalAlias: RoomAlias?, val avatarUrl: String?, - val isTombstoned: Boolean, val memberCount: Long, val showRolesAndPermissions: Boolean, val showSecurityAndPrivacy: Boolean, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt index e248c1798a..db1b336653 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt @@ -27,7 +27,6 @@ fun aSpaceSettingsState( alias: RoomAlias? = RoomAlias("#spacename:element.io"), avatarUrl: String? = null, memberCount: Long = 100, - isTombstoned: Boolean = false, showRolesAndPermissions: Boolean = false, showSecurityAndPrivacy: Boolean = false, eventSink: (SpaceSettingsEvents) -> Unit = {}, @@ -36,7 +35,6 @@ fun aSpaceSettingsState( name = name, canonicalAlias = alias, avatarUrl = avatarUrl, - isTombstoned = isTombstoned, memberCount = memberCount, showRolesAndPermissions = showRolesAndPermissions, showSecurityAndPrivacy = showSecurityAndPrivacy, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt index eaefcf59d5..1775b1e5b1 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt @@ -41,13 +41,14 @@ import io.element.android.libraries.designsystem.theme.components.ListItemStyle import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings @Composable fun SpaceSettingsView( state: SpaceSettingsState, onBackClick: () -> Unit, - onSpaceInfoClick: ()->Unit, + onSpaceInfoClick: () -> Unit, onMembersClick: () -> Unit, onRolesAndPermissionsClick: () -> Unit, onSecurityAndPrivacyClick: () -> Unit, @@ -65,59 +66,74 @@ fun SpaceSettingsView( .padding(padding) .verticalScroll(rememberScrollState()) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onSpaceInfoClick) - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Avatar( - avatarData = AvatarData(state.roomId.value, state.name, state.avatarUrl, AvatarSize.SpaceListItem), - avatarType = AvatarType.Space( - isTombstoned = state.isTombstoned, - ), - contentDescription = state.avatarUrl?.let { stringResource(CommonStrings.a11y_room_avatar) }, - ) - Spacer(Modifier.width(16.dp)) - Column { - Text( - text = state.name, - style = ElementTheme.typography.fontHeadingMdRegular, - color = ElementTheme.colors.textPrimary, - ) - if (state.canonicalAlias != null) { - Text( - text = state.canonicalAlias.value, - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textSecondary, - ) - } - } - } - Section(isVisible = state.showSecurityAndPrivacy) { + SpaceInfoSection( + roomId = state.roomId, + name = state.name, + avatarUrl = state.avatarUrl, + canonicalAlias = state.canonicalAlias?.value, + onSpaceInfoClick = onSpaceInfoClick, + ) + Section(isVisible = state.showSecurityAndPrivacy, content = { SecurityAndPrivacyItem( onClick = onSecurityAndPrivacyClick ) - } - Section { + }) + Section(content = { MembersItem(state.memberCount, onClick = onMembersClick) if (state.showRolesAndPermissions) { RolesAndPermissionsItem(onClick = onRolesAndPermissionsClick) } - } - Section { + }) + Section(content = { LeaveSpaceItem( onClick = onLeaveSpaceClick ) - } + }) } } } @Composable -private fun ColumnScope.Section( +private fun SpaceInfoSection( + roomId: RoomId, + name: String, + avatarUrl: String?, + canonicalAlias: String?, + onSpaceInfoClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onSpaceInfoClick) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + avatarData = AvatarData(roomId.value, name, avatarUrl, AvatarSize.SpaceListItem), + avatarType = AvatarType.Space(), + contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_avatar) }, + ) + Spacer(Modifier.width(16.dp)) + Column { + Text( + text = name, + style = ElementTheme.typography.fontHeadingMdRegular, + color = ElementTheme.colors.textPrimary, + ) + if (canonicalAlias != null) { + Text( + text = canonicalAlias, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + ) + } + } + } +} + +@Composable +private fun Section( modifier: Modifier = Modifier, isVisible: Boolean = true, content: @Composable ColumnScope.() -> Unit, From 2d8f491cfddcd22e4ee574dea369b9f7381d4e55 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 31 Oct 2025 14:55:07 +0100 Subject: [PATCH 19/58] feature(space) : update some strings --- .../impl/src/main/res/values/localazy.xml | 2 +- features/roomdetails/impl/src/main/res/values/localazy.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 ++ .../io/element/android/features/space/impl/root/SpaceView.kt | 2 +- .../features/space/impl/settings/SpaceSettingsView.kt | 5 +++-- features/space/impl/src/main/res/values/localazy.xml | 3 +++ libraries/ui-strings/src/main/res/values/localazy.xml | 2 +- tools/localazy/config.json | 3 ++- 8 files changed, 14 insertions(+), 7 deletions(-) diff --git a/features/changeroommemberroles/impl/src/main/res/values/localazy.xml b/features/changeroommemberroles/impl/src/main/res/values/localazy.xml index 456426726a..43f6dc10f8 100644 --- a/features/changeroommemberroles/impl/src/main/res/values/localazy.xml +++ b/features/changeroommemberroles/impl/src/main/res/values/localazy.xml @@ -33,7 +33,7 @@ "Members" "You have unsaved changes." "Save changes?" - "There are no banned users in this room." + "There are no banned users." "%1$d person" "%1$d people" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index ce8eb3a7b7..814f352abd 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -70,7 +70,7 @@ "Room info" "Topic" "Updating room…" - "There are no banned users in this room." + "There are no banned users." "%1$d person" "%1$d people" diff --git a/features/roommembermoderation/impl/src/main/res/values/localazy.xml b/features/roommembermoderation/impl/src/main/res/values/localazy.xml index e3f6071898..3d23c8763a 100644 --- a/features/roommembermoderation/impl/src/main/res/values/localazy.xml +++ b/features/roommembermoderation/impl/src/main/res/values/localazy.xml @@ -4,10 +4,12 @@ "Ban" "They won’t be able to join again if invited." "Are you sure you want to ban this member?" + "They won’t be able to join this space again if invited, but they’ll still keep their memberships of any rooms or subspaces." "Banning %1$s" "Remove" "They will be able to join this room again if invited." "Are you sure you want to remove this member?" + "They will be able to join this space again if invited, and they’ll still keep their memberships of any rooms or subspaces." "View profile" "Remove user" "Remove member and ban from joining in the future?" diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index f4c415b718..2779ab2687 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -328,7 +328,7 @@ private fun SpaceViewTopBar( }, text = { Text( - text = stringResource(id = CommonStrings.action_leave), + text = stringResource(id = CommonStrings.action_leave_space), color = ElementTheme.colors.textCriticalPrimary, ) }, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt index 1775b1e5b1..9689b9e4f1 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.space.impl.R import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -162,7 +163,7 @@ private fun SecurityAndPrivacyItem( modifier: Modifier = Modifier, ) { ListItem( - headlineContent = { Text("Security & privacy") }, + headlineContent = { Text(stringResource(R.string.screen_space_settings_security_and_privacy)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())), onClick = onClick, modifier = modifier, @@ -190,7 +191,7 @@ private fun RolesAndPermissionsItem( modifier: Modifier = Modifier, ) { ListItem( - headlineContent = { Text("Roles & permissions") }, + headlineContent = { Text(stringResource(R.string.screen_space_settings_roles_and_permissions)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Admin())), onClick = onClick, modifier = modifier, diff --git a/features/space/impl/src/main/res/values/localazy.xml b/features/space/impl/src/main/res/values/localazy.xml index c6ced29d41..a4df5e767d 100644 --- a/features/space/impl/src/main/res/values/localazy.xml +++ b/features/space/impl/src/main/res/values/localazy.xml @@ -10,4 +10,7 @@ "You will not be removed from the following room(s) because you\'re the only administrator:" "Leave %1$s?" "You are the only admin for %1$s" + "Leave space" + "Roles & permissions" + "Security & privacy" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 8ca9198cfe..7bf9e8192c 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -95,6 +95,7 @@ "Forgot password?" "Forward" "Go back" + "Go to roles & permissions" "Go to settings" "Ignore" "Invite" @@ -176,7 +177,6 @@ "Advanced settings" "an image" "Analytics" - "Fetching notifications…" "You left the room" "You were logged out of the session" "Appearance" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 82c273db2e..3039631253 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -210,7 +210,8 @@ { "name" : ":features:space:impl", "includeRegex" : [ - "screen\\.leave_space\\..*" + "screen\\.leave_space\\..*", + "screen\\.space_settings\\..*" ] }, { From f844007812bb754df4276fc56b47fe28ac6c9e23 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 31 Oct 2025 15:10:55 +0100 Subject: [PATCH 20/58] feature(space) : some renaming on Space nodes --- .../features/space/impl/SpaceFlowNode.kt | 12 +++++----- .../space/impl/settings/SpaceSettingsNode.kt | 24 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt index 6684d07952..ba55203be6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -106,27 +106,27 @@ class SpaceFlowNode( } NavTarget.Settings -> { val callback = object : SpaceSettingsNode.Callback { - override fun onBackClick() { + override fun closeSettings() { backstack.pop() } - override fun onSpaceInfoClick() { + override fun navigateToSpaceInfo() { //TODO } - override fun onMembersClick() { + override fun navigateToSpaceMembers() { callback.navigateToRoomMemberList() } - override fun onRolesAndPermissionsClick() { + override fun navigateToRolesAndPermissions() { //TODO } - override fun onSecurityAndPrivacyClick() { + override fun navigateToSecurityAndPrivacy() { //TODO } - override fun onLeaveSpaceClick() { + override fun startLeaveSpaceFlow() { backstack.push(NavTarget.Leave) } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt index 77ae924f94..9b81272153 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt @@ -29,13 +29,13 @@ class SpaceSettingsNode( private val presenter: SpaceSettingsPresenter, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun onBackClick() + fun closeSettings() - fun onSpaceInfoClick() - fun onMembersClick() - fun onRolesAndPermissionsClick() - fun onSecurityAndPrivacyClick() - fun onLeaveSpaceClick() + fun navigateToSpaceInfo() + fun navigateToSpaceMembers() + fun navigateToRolesAndPermissions() + fun navigateToSecurityAndPrivacy() + fun startLeaveSpaceFlow() } private val callback = plugins().single() @@ -47,12 +47,12 @@ class SpaceSettingsNode( SpaceSettingsView( state = state, modifier = modifier, - onSpaceInfoClick = callback::onSpaceInfoClick, - onBackClick = callback::onBackClick, - onMembersClick = callback::onMembersClick, - onRolesAndPermissionsClick = callback::onRolesAndPermissionsClick, - onSecurityAndPrivacyClick = callback::onSecurityAndPrivacyClick, - onLeaveSpaceClick = callback::onLeaveSpaceClick, + onSpaceInfoClick = callback::navigateToSpaceInfo, + onBackClick = callback::closeSettings, + onMembersClick = callback::navigateToSpaceMembers, + onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions, + onSecurityAndPrivacyClick = callback::navigateToSecurityAndPrivacy, + onLeaveSpaceClick = callback::startLeaveSpaceFlow, ) } } From 48d2696ba87e41b13ac11b63bc41289c1834dbc1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 31 Oct 2025 15:32:30 +0100 Subject: [PATCH 21/58] feature(space) : prepare LeaveSpace for navigation to Roles&Permissions --- .../features/space/impl/SpaceFlowNode.kt | 17 +++++++++++++---- .../features/space/impl/leave/LeaveSpaceNode.kt | 11 ++++++++++- .../features/space/impl/leave/LeaveSpaceView.kt | 17 +++++++++++++++-- .../space/impl/settings/SpaceSettingsView.kt | 1 - 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt index ba55203be6..2df48350c6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -82,7 +82,16 @@ class SpaceFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Leave -> { - createNode(buildContext) + val callback = object : LeaveSpaceNode.Callback { + override fun closeLeaveSpaceFlow() { + backstack.pop() + } + + override fun navigateToRolesAndPermissions() { + // TODO + } + } + createNode(buildContext, listOf(callback)) } NavTarget.Root -> { val callback = object : SpaceNode.Callback { @@ -111,7 +120,7 @@ class SpaceFlowNode( } override fun navigateToSpaceInfo() { - //TODO + // TODO } override fun navigateToSpaceMembers() { @@ -119,11 +128,11 @@ class SpaceFlowNode( } override fun navigateToRolesAndPermissions() { - //TODO + // TODO } override fun navigateToSecurityAndPrivacy() { - //TODO + // TODO } override fun startLeaveSpaceFlow() { diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt index 215f06bca5..6ba86481fe 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt @@ -13,6 +13,7 @@ import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode @@ -29,9 +30,16 @@ class LeaveSpaceNode( room: JoinedRoom, presenterFactory: LeaveSpacePresenter.Factory, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun closeLeaveSpaceFlow() + fun navigateToRolesAndPermissions() + } + private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId) private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle) + private val callback = plugins().single() + override fun onBuilt() { super.onBuilt() lifecycle.subscribe( @@ -46,7 +54,8 @@ class LeaveSpaceNode( val state = presenter.present() LeaveSpaceView( state = state, - onCancel = ::navigateUp, + onCancel = callback::closeLeaveSpaceFlow, + onRolesAndPermissionsClick = callback::navigateToRolesAndPermissions, modifier = modifier ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt index 7432301f91..6f7a9903df 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt @@ -69,6 +69,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun LeaveSpaceView( state: LeaveSpaceState, onCancel: () -> Unit, + onRolesAndPermissionsClick: () -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -130,6 +131,9 @@ fun LeaveSpaceView( state.eventSink(LeaveSpaceEvents.LeaveSpace) }, onCancel = onCancel, + // TODO enable when navigation is ready + showRolesAndPermissionsButton = false, // state.isLastAdmin, + onRolesAndPermissionsClick = onRolesAndPermissionsClick, ) } } @@ -210,6 +214,8 @@ private fun LeaveSpaceButtons( showLeaveButton: Boolean, selectedRoomsCount: Int, onLeaveSpace: () -> Unit, + showRolesAndPermissionsButton: Boolean, + onRolesAndPermissionsClick: () -> Unit, onCancel: () -> Unit, ) { ButtonColumnMolecule( @@ -229,8 +235,14 @@ private fun LeaveSpaceButtons( destructive = true, ) } - // TODO For least admin space, add a button to open the settings. - // See https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=4622-59600 + if (showRolesAndPermissionsButton) { + Button( + text = stringResource(CommonStrings.action_go_to_roles_and_permissions), + onClick = onRolesAndPermissionsClick, + modifier = Modifier.fillMaxWidth(), + leadingIcon = IconSource.Vector(CompoundIcons.Settings()), + ) + } TextButton( modifier = Modifier.fillMaxWidth(), text = stringResource(CommonStrings.action_cancel), @@ -345,5 +357,6 @@ internal fun LeaveSpaceViewPreview( LeaveSpaceView( state = state, onCancel = {}, + onRolesAndPermissionsClick = {}, ) } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt index 9689b9e4f1..fae5bf2f03 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt @@ -90,7 +90,6 @@ fun SpaceSettingsView( onClick = onLeaveSpaceClick ) }) - } } } From 4097ff26268c35dfd87b69c3d434f2c8b81486cf Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 31 Oct 2025 15:34:55 +0100 Subject: [PATCH 22/58] feature(space) : some code clean up --- .../io/element/android/features/space/impl/SpaceFlowNode.kt | 4 ++-- .../android/features/space/impl/DefaultSpaceEntryPointTest.kt | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt index 2df48350c6..686729bae5 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -35,7 +35,7 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.room.BaseRoom +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.spaces.SpaceService import kotlinx.parcelize.Parcelize @@ -44,7 +44,7 @@ import kotlinx.parcelize.Parcelize class SpaceFlowNode( @Assisted val buildContext: BuildContext, @Assisted plugins: List, - room: BaseRoom, + room: JoinedRoom, spaceService: SpaceService, graphFactory: SpaceFlowGraph.Factory, ) : BaseFlowNode( diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt index 3fd260dd4f..696e80eeea 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt @@ -15,6 +15,8 @@ import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.features.space.impl.di.FakeSpaceFlowGraph import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.tests.testutils.lambda.lambdaError @@ -40,12 +42,12 @@ class DefaultSpaceEntryPointTest { spaceService = FakeSpaceService( spaceRoomListResult = { _: RoomId -> FakeSpaceRoomList(A_ROOM_ID) } ), + room = FakeJoinedRoom(), graphFactory = FakeSpaceFlowGraph.Factory ) } val callback = object : SpaceEntryPoint.Callback { override fun navigateToRoom(roomId: RoomId, viaParameters: List) = lambdaError() - override fun navigateToRoomDetails() = lambdaError() override fun navigateToRoomMemberList() = lambdaError() } val result = entryPoint.createNode( From e6934aacfad9e42a8cba82e75d71c885717c9f8e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Oct 2025 19:15:19 +0100 Subject: [PATCH 23/58] Improve code. --- .../DefaultNotificationDrawerManager.kt | 25 +------------------ .../factories/NotificationCreator.kt | 4 ++- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index f3ba1c6d84..1677072399 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -10,10 +10,8 @@ package io.element.android.libraries.push.impl.notifications import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn -import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.annotations.AppCoroutineScope -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -31,7 +29,6 @@ import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.currentSessionId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag) @@ -190,29 +187,9 @@ class DefaultNotificationDrawerManager( // We have an avatar and a display name, use it userFromCache } else { - client.getSafeUserProfile() + client.getUserProfile().getOrNull() ?: MatrixUser(sessionId) } - notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents, imageLoader) } } - - private suspend fun MatrixClient.getSafeUserProfile(): MatrixUser { - return tryOrNull( - onException = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, - operation = { - val profile = getUserProfile().getOrNull() - // displayName cannot be empty else NotificationCompat.MessagingStyle() will crash - if (profile?.displayName.isNullOrEmpty()) { - profile?.copy(displayName = sessionId.value) - } else { - profile - } - } - ) ?: MatrixUser( - userId = sessionId, - displayName = sessionId.value, - avatarUrl = null - ) - } } 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 390f6b66d7..37774b4cc7 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 @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.model.getBestName import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo @@ -446,7 +447,8 @@ class DefaultNotificationCreator( ): MessagingStyle { return MessagingStyle( Person.Builder() - .setName(user.displayName?.annotateForDebug(50)) + // Note: name cannot be empty else NotificationCompat.MessagingStyle() will crash + .setName(user.getBestName().annotateForDebug(50)) .setIcon(bitmapLoader.getUserIcon(user.avatarUrl, imageLoader)) .setKey(user.userId.value) .build() From d373cbe8a7ecc8f337d577d51d775a4e90342c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 3 Nov 2025 14:43:18 +0100 Subject: [PATCH 24/58] Create `PinnedEventsTimelineProvider`, expose it in `TimelineBindings` --- .../kotlin/io/element/android/x/di/RoomGraph.kt | 3 ++- .../android/appnav/di/TimelineBindings.kt | 16 ++++++++++++++++ .../api/pinned/PinnedEventsTimelineProvider.kt | 12 ++++++++++++ ...kt => DefaultPinnedEventsTimelineProvider.kt} | 9 ++++++--- 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/di/TimelineBindings.kt create mode 100644 features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/PinnedEventsTimelineProvider.kt rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/{PinnedEventsTimelineProvider.kt => DefaultPinnedEventsTimelineProvider.kt} (91%) diff --git a/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt b/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt index e48dd52daf..c9dfd266d9 100644 --- a/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt +++ b/app/src/main/kotlin/io/element/android/x/di/RoomGraph.kt @@ -9,13 +9,14 @@ package io.element.android.x.di import dev.zacsweers.metro.GraphExtension import dev.zacsweers.metro.Provides +import io.element.android.appnav.di.TimelineBindings import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.JoinedRoom @GraphExtension(RoomScope::class) -interface RoomGraph : NodeFactoriesBindings { +interface RoomGraph : NodeFactoriesBindings, TimelineBindings { @GraphExtension.Factory interface Factory { fun create( diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/TimelineBindings.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/TimelineBindings.kt new file mode 100644 index 0000000000..e338f2ba8e --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/TimelineBindings.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.appnav.di + +import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider +import io.element.android.libraries.matrix.api.timeline.TimelineProvider + +interface TimelineBindings { + val timelineProvider: TimelineProvider + val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider +} diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/PinnedEventsTimelineProvider.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/PinnedEventsTimelineProvider.kt new file mode 100644 index 0000000000..026486e00a --- /dev/null +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/PinnedEventsTimelineProvider.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.api.pinned + +import io.element.android.libraries.matrix.api.timeline.TimelineProvider + +interface PinnedEventsTimelineProvider : TimelineProvider diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultPinnedEventsTimelineProvider.kt similarity index 91% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultPinnedEventsTimelineProvider.kt index 811516e022..0e2de50b86 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/PinnedEventsTimelineProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultPinnedEventsTimelineProvider.kt @@ -7,8 +7,11 @@ package io.element.android.features.messages.impl.pinned +import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding +import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.mapState @@ -17,7 +20,6 @@ import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.timeline.TimelineProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -29,12 +31,13 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext @SingleIn(RoomScope::class) +@ContributesBinding(RoomScope::class, binding = binding()) @Inject -class PinnedEventsTimelineProvider( +class DefaultPinnedEventsTimelineProvider( private val room: JoinedRoom, private val syncService: SyncService, private val dispatchers: CoroutineDispatchers, -) : TimelineProvider { +) : PinnedEventsTimelineProvider { private val _timelineStateFlow: MutableStateFlow> = MutableStateFlow(AsyncData.Uninitialized) From 7421e6545c603640fb5eb75463abd1a9e5972832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 3 Nov 2025 14:43:57 +0100 Subject: [PATCH 25/58] Pass around `fromPinnedEvents` so we can use it to decide whether to forward the event from the main timeline or the pinned events one --- .../features/messages/api/MessagesEntryPoint.kt | 2 +- .../android/features/messages/impl/MessagesFlowNode.kt | 8 ++++---- .../pinned/banner/PinnedMessagesBannerPresenter.kt | 4 ++-- .../impl/pinned/list/PinnedMessagesListPresenter.kt | 4 ++-- .../pinned/banner/PinnedMessagesBannerPresenterTest.kt | 4 ++-- .../pinned/list/PinnedMessagesListPresenterTest.kt | 4 ++-- .../features/roomdetails/api/RoomDetailsEntryPoint.kt | 2 +- .../features/roomdetails/impl/RoomDetailsFlowNode.kt | 10 +++++----- .../features/userprofile/impl/UserProfileFlowNode.kt | 2 +- .../mediaviewer/api/MediaGalleryEntryPoint.kt | 2 +- .../libraries/mediaviewer/api/MediaViewerEntryPoint.kt | 2 +- .../impl/datasource/MediaGalleryDataSource.kt | 5 +++++ .../impl/gallery/root/MediaGalleryFlowNode.kt | 6 +++--- .../mediaviewer/impl/viewer/MediaViewerDataSource.kt | 2 ++ .../mediaviewer/impl/viewer/MediaViewerNavigator.kt | 2 +- .../mediaviewer/impl/viewer/MediaViewerNode.kt | 4 ++-- .../mediaviewer/impl/viewer/MediaViewerPresenter.kt | 2 +- .../impl/viewer/SingleMediaGalleryDataSource.kt | 2 ++ 18 files changed, 38 insertions(+), 29 deletions(-) diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index 54df2b7fec..901a243945 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -35,7 +35,7 @@ interface MessagesEntryPoint : FeatureEntryPoint { fun navigateToRoomDetails() fun navigateToRoomMemberDetails(userId: UserId) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) - fun forwardEvent(eventId: EventId) + fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) fun navigateToRoom(roomId: RoomId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 7d468354d8..b4320ab69e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -35,7 +35,7 @@ import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPointNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode -import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider +import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider import io.element.android.features.messages.impl.pinned.list.PinnedMessagesListNode import io.element.android.features.messages.impl.report.ReportMessageNode import io.element.android.features.messages.impl.threads.ThreadedMessagesNode @@ -115,7 +115,7 @@ class MessagesFlowNode( private val roomNamesCache: RoomNamesCache, private val mentionSpanUpdater: MentionSpanUpdater, private val mentionSpanTheme: MentionSpanTheme, - private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, + private val pinnedEventsTimelineProvider: DefaultPinnedEventsTimelineProvider, private val timelineController: TimelineController, private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, private val dateFormatter: DateFormatter, @@ -315,9 +315,9 @@ class MessagesFlowNode( this@MessagesFlowNode.viewInTimeline(eventId) } - override fun forwardEvent(eventId: EventId) { + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) { // Need to go to the parent because of the overlay - callback.forwardEvent(eventId) + callback.forwardEvent(eventId, fromPinnedEvents) } } mediaViewerEntryPoint.createNode( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index 5833da56dc..c0f2cccb6f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt @@ -18,7 +18,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject -import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider +import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.room.BaseRoom @@ -35,7 +35,7 @@ import kotlinx.coroutines.flow.onEach class PinnedMessagesBannerPresenter( private val room: BaseRoom, private val itemFactory: PinnedMessagesBannerItemFactory, - private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, + private val pinnedEventsTimelineProvider: DefaultPinnedEventsTimelineProvider, ) : Presenter { private val pinnedItems = mutableStateOf>>(AsyncData.Uninitialized) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index 6d09e12447..764286ce2e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -26,7 +26,7 @@ import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.link.LinkState -import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider +import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig @@ -66,7 +66,7 @@ class PinnedMessagesListPresenter( @Assisted private val navigator: PinnedMessagesListNavigator, private val room: JoinedRoom, timelineItemsFactoryCreator: TimelineItemsFactory.Creator, - private val timelineProvider: PinnedEventsTimelineProvider, + private val timelineProvider: DefaultPinnedEventsTimelineProvider, private val timelineProtectionPresenter: Presenter, private val linkPresenter: Presenter, private val snackbarDispatcher: SnackbarDispatcher, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt index 38182dec1d..b1be33d928 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt @@ -8,7 +8,7 @@ package io.element.android.features.messages.impl.pinned.banner import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider +import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.sync.SyncService @@ -195,7 +195,7 @@ class PinnedMessagesBannerPresenterTest { internal fun TestScope.createPinnedEventsTimelineProvider( room: JoinedRoom = FakeJoinedRoom(), syncService: SyncService = FakeSyncService(), -) = PinnedEventsTimelineProvider( +) = DefaultPinnedEventsTimelineProvider( room = room, syncService = syncService, dispatchers = testCoroutineDispatchers(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index 07778ab381..5087bd3558 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -13,7 +13,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator import io.element.android.features.messages.impl.link.aLinkState -import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider +import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -300,7 +300,7 @@ class PinnedMessagesListPresenterTest { analyticsService: AnalyticsService = FakeAnalyticsService(), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), ): PinnedMessagesListPresenter { - val timelineProvider = PinnedEventsTimelineProvider( + val timelineProvider = DefaultPinnedEventsTimelineProvider( room = room, syncService = syncService, dispatchers = testCoroutineDispatchers(), diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index 3a690ddea6..96ff1ae96e 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -40,7 +40,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { fun navigateToGlobalNotificationSettings() fun navigateToRoom(roomId: RoomId, serverNames: List) fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) - fun startForwardEventFlow(eventId: EventId) + fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) } fun createNode( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index d31adaae8d..5eee694786 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -299,7 +299,7 @@ class RoomDetailsFlowNode( // Cannot happen } - override fun forwardEvent(eventId: EventId) { + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) { // Cannot happen } } @@ -331,8 +331,8 @@ class RoomDetailsFlowNode( callback.handlePermalinkClick(permalinkData, pushToBackstack = false) } - override fun forward(eventId: EventId) { - callback.startForwardEventFlow(eventId) + override fun forward(eventId: EventId, fromPinnedEvents: Boolean) { + callback.startForwardEventFlow(eventId, fromPinnedEvents) } } mediaGalleryEntryPoint.createNode( @@ -358,8 +358,8 @@ class RoomDetailsFlowNode( callback.handlePermalinkClick(data, pushToBackstack) } - override fun forwardEvent(eventId: EventId) { - callback.startForwardEventFlow(eventId) + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) { + callback.startForwardEventFlow(eventId, fromPinnedEvents) } override fun navigateToRoom(roomId: RoomId) { diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt index fd019c3587..0b36452b45 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt @@ -103,7 +103,7 @@ class UserProfileFlowNode( // Cannot happen } - override fun forwardEvent(eventId: EventId) { + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) { // Cannot happen } } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt index dd71f302d4..2fdcaf1bd0 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt @@ -23,6 +23,6 @@ interface MediaGalleryEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onBackClick() fun viewInTimeline(eventId: EventId) - fun forward(eventId: EventId) + fun forward(eventId: EventId, fromPinnedEvents: Boolean) } } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt index 201573dc70..ab1c78abea 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt @@ -31,7 +31,7 @@ interface MediaViewerEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onDone() fun viewInTimeline(eventId: EventId) - fun forwardEvent(eventId: EventId) + fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) } data class Params( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt index eb822b4969..46bfedacb1 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt @@ -32,6 +32,7 @@ interface MediaGalleryDataSource { fun start() fun groupedMediaItemsFlow(): Flow> fun getLastData(): AsyncData + fun fromPinnedEvents(): Boolean suspend fun loadMore(direction: Timeline.PaginationDirection) suspend fun deleteItem(eventId: EventId) } @@ -46,6 +47,10 @@ class TimelineMediaGalleryDataSource( ) : MediaGalleryDataSource { private var timeline: Timeline? = null + override fun fromPinnedEvents(): Boolean { + return timeline?.mode == Timeline.Mode.PinnedEvents + } + private val groupedMediaItemsFlow = MutableSharedFlow>(replay = 1) override fun groupedMediaItemsFlow(): Flow> = groupedMediaItemsFlow diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt index cc71b49dac..e21da04586 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryFlowNode.kt @@ -85,7 +85,7 @@ class MediaGalleryFlowNode( } override fun forward(eventId: EventId) { - callback.forward(eventId) + callback.forward(eventId, fromPinnedEvents = false) } override fun showItem(item: MediaItem.Event) { @@ -119,9 +119,9 @@ class MediaGalleryFlowNode( callback.viewInTimeline(eventId) } - override fun forwardEvent(eventId: EventId) { + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) { // Need to go to the parent because of the overlay - callback.forward(eventId) + callback.forward(eventId, fromPinnedEvents) } } mediaViewerEntryPoint.createNode( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index 1046a80fe3..62d87a770a 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -61,6 +61,8 @@ class MediaViewerDataSource( private val localMediaStates: MutableMap>> = mutableMapOf() + fun fromPinnedEvents(): Boolean = galleryDataSource.fromPinnedEvents() + fun setup() { galleryDataSource.start() } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt index 77e253dfa5..b3fa321170 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt @@ -11,6 +11,6 @@ import io.element.android.libraries.matrix.api.core.EventId interface MediaViewerNavigator { fun onViewInTimelineClick(eventId: EventId) - fun onForwardClick(eventId: EventId) + fun onForwardClick(eventId: EventId, fromPinnedEvents: Boolean) fun onItemDeleted() } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index 79a95c4648..195f1589d6 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -64,8 +64,8 @@ class MediaViewerNode( callback.viewInTimeline(eventId) } - override fun onForwardClick(eventId: EventId) { - callback.forwardEvent(eventId) + override fun onForwardClick(eventId: EventId, fromPinnedEvents: Boolean) { + callback.forwardEvent(eventId, fromPinnedEvents) } override fun onItemDeleted() { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index 726e9989ce..be10af3f6d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -119,7 +119,7 @@ class MediaViewerPresenter( } is MediaViewerEvents.Forward -> { mediaBottomSheetState = MediaBottomSheetState.Hidden - navigator.onForwardClick(event.eventId) + navigator.onForwardClick(event.eventId, fromPinnedEvents = dataSource.fromPinnedEvents()) } is MediaViewerEvents.OpenInfo -> coroutineScope.launch { mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt index 94ac0fea21..1d683fd8da 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt @@ -27,6 +27,8 @@ class SingleMediaGalleryDataSource( override fun start() = Unit override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) override fun getLastData(): AsyncData = AsyncData.Success(data) + override fun fromPinnedEvents(): Boolean = false + override suspend fun loadMore(direction: Timeline.PaginationDirection) = Unit override suspend fun deleteItem(eventId: EventId) = Unit From 0bed2ae386126756b22f16bda077a6181854e302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 3 Nov 2025 14:44:29 +0100 Subject: [PATCH 26/58] Use the new `fromPinnedEvents` parameter in `JoinedRoomLoadedFlowNode` through `TimelineBindings` --- .../room/joined/JoinedRoomLoadedFlowNode.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 16eaff89b1..8216ee512c 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -22,10 +22,12 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.appnav.di.RoomGraphFactory +import io.element.android.appnav.di.TimelineBindings import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPointNode +import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.BackstackView @@ -44,6 +46,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.JoinedRoom +import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope @@ -136,8 +139,8 @@ class JoinedRoomLoadedFlowNode( callback.handlePermalinkClick(data, pushToBackstack) } - override fun startForwardEventFlow(eventId: EventId) { - backstack.push(NavTarget.ForwardEvent(eventId)) + override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) { + backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents)) } } return roomDetailsEntryPoint.createNode( @@ -169,7 +172,11 @@ class JoinedRoomLoadedFlowNode( createSpaceNode(buildContext) } is NavTarget.ForwardEvent -> { - val timelineProvider = { MutableStateFlow(inputs.room.liveTimeline).asStateFlow() } + val timelineProvider = if (navTarget.fromPinnedEvents) { + (graph as TimelineBindings).pinnedEventsTimelineProvider + } else { + (graph as TimelineBindings).timelineProvider + } val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider) val callback = object : ForwardEntryPoint.Callback { override fun onDone(roomIds: List) { @@ -228,8 +235,8 @@ class JoinedRoomLoadedFlowNode( callback.handlePermalinkClick(data, pushToBackstack) } - override fun forwardEvent(eventId: EventId) { - backstack.push(NavTarget.ForwardEvent(eventId)) + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) { + backstack.push(NavTarget.ForwardEvent(eventId, fromPinnedEvents)) } override fun navigateToRoom(roomId: RoomId) { @@ -266,7 +273,7 @@ class JoinedRoomLoadedFlowNode( data class RoomMemberDetails(val userId: UserId) : NavTarget @Parcelize - data class ForwardEvent(val eventId: EventId) : NavTarget + data class ForwardEvent(val eventId: EventId, val fromPinnedEvents: Boolean) : NavTarget @Parcelize data object RoomNotificationSettings : NavTarget From 19b0e27ab20f1e919c3b11556fb19d50b070aeb4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 14:58:40 +0100 Subject: [PATCH 27/58] Update notification sound --- .../push/impl/src/main/res/raw/message.mp3 | Bin 22414 -> 8685 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/libraries/push/impl/src/main/res/raw/message.mp3 b/libraries/push/impl/src/main/res/raw/message.mp3 index 5e9645adaf1f2b7435ef29360c690b8ddc405d3a..abc056786c8504a9441ed74e955d9dcfa6bac927 100644 GIT binary patch literal 8685 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

q)t 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 literal 22414 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 Date: Mon, 3 Nov 2025 14:16:13 +0000 Subject: [PATCH 28/58] Update screenshots --- ...features.space.impl.settings_SpaceSettingsView_Day_0_en.png | 3 +++ ...features.space.impl.settings_SpaceSettingsView_Day_1_en.png | 3 +++ ...features.space.impl.settings_SpaceSettingsView_Day_2_en.png | 3 +++ ...features.space.impl.settings_SpaceSettingsView_Day_3_en.png | 3 +++ ...atures.space.impl.settings_SpaceSettingsView_Night_0_en.png | 3 +++ ...atures.space.impl.settings_SpaceSettingsView_Night_1_en.png | 3 +++ ...atures.space.impl.settings_SpaceSettingsView_Night_2_en.png | 3 +++ ...atures.space.impl.settings_SpaceSettingsView_Night_3_en.png | 3 +++ 8 files changed, 24 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_0_en.png new file mode 100644 index 0000000000..24c00cb1a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a573dcac3bab78db152da81c0a1b7803fcf70c2392cc05a20ae533edfde76730 +size 21620 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_1_en.png new file mode 100644 index 0000000000..13d9ea54d2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a17f1005bf296f04a2d631c6d19313bc864ebcd6832a788f8fa6909b7562dcb5 +size 17874 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_2_en.png new file mode 100644 index 0000000000..f408da9f38 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc34deb76678be310df0beb512d91fa3b6edf2d2c59a7daf46267f1c8012e12c +size 25422 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_3_en.png new file mode 100644 index 0000000000..189eb42ad0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14d4cabaf98490e35bb75a12c3d37e3c499158b6d6b639084059ee1bc999e831 +size 26018 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_0_en.png new file mode 100644 index 0000000000..d163b60818 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b13402b213b595b8a0a632eeb96263db7e8f7cbd3f56d8d235c38abf6df66128 +size 21235 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_1_en.png new file mode 100644 index 0000000000..77db74b048 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7031872033a6c12acd7328a5ae0820ac2dfa00340f8a8b6cac746c98004b285c +size 17444 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_2_en.png new file mode 100644 index 0000000000..9d45abb1c6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:053e0d74fb1c01e5e434aa48606b5f0d24fb9a950b56555783b618679c4e7ad5 +size 24925 diff --git a/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_3_en.png new file mode 100644 index 0000000000..9c36d80cef --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.space.impl.settings_SpaceSettingsView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d201e92db03d2d0d1b7d786d472d5d0f3e946ba7b610b9769ceb597b4d827eb9 +size 25467 From 876f692f3f90b5656180371cd7e0b1059689b832 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Oct 2025 15:02:26 +0100 Subject: [PATCH 29/58] Rename MessagesEntryPointNode to MessagesEntryPoint.NodeProxy for consistency. We already have a NodeProxy. --- .../android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt | 3 +-- .../android/features/messages/api/MessagesEntryPoint.kt | 6 +++--- .../android/features/messages/impl/MessagesFlowNode.kt | 4 +--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 8216ee512c..2faed24a19 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -26,7 +26,6 @@ import io.element.android.appnav.di.TimelineBindings import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint -import io.element.android.features.messages.api.MessagesEntryPointNode import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.space.api.SpaceEntryPoint @@ -283,7 +282,7 @@ class JoinedRoomLoadedFlowNode( val messageNode = waitForChildAttached { navTarget -> navTarget is NavTarget.Messages } - (messageNode as? MessagesEntryPointNode)?.attachThread(threadId, focusedEventId) + (messageNode as? MessagesEntryPoint.NodeProxy)?.attachThread(threadId, focusedEventId) } @Composable diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index 901a243945..99f9ed79a0 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -47,8 +47,8 @@ interface MessagesEntryPoint : FeatureEntryPoint { params: Params, callback: Callback, ): Node -} -interface MessagesEntryPointNode { - suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) + interface NodeProxy { + suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index b4320ab69e..d51ebd250e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -32,7 +32,6 @@ import io.element.android.features.location.api.LocationService import io.element.android.features.location.api.SendLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint -import io.element.android.features.messages.api.MessagesEntryPointNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider @@ -130,8 +129,7 @@ class MessagesFlowNode( ), buildContext = buildContext, plugins = plugins, -), - MessagesEntryPointNode { +), MessagesEntryPoint.NodeProxy { sealed interface NavTarget : Parcelable { @Parcelize data class Messages(val focusedEventId: EventId?) : NavTarget From bb28fd6e9243b573bd91eee594b3d9b72aa2e02b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 31 Oct 2025 17:03:54 +0100 Subject: [PATCH 30/58] Forward Event error: Log error and use generic error dialog. --- features/forward/impl/build.gradle.kts | 1 + .../features/forward/impl/ForwardMessagesPresenter.kt | 7 ++++++- .../android/features/forward/impl/ForwardMessagesView.kt | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/features/forward/impl/build.gradle.kts b/features/forward/impl/build.gradle.kts index de38d25651..0364ffadd8 100644 --- a/features/forward/impl/build.gradle.kts +++ b/features/forward/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.matrix.api) implementation(projects.libraries.roomselect.api) + implementation(projects.libraries.uiStrings) testCommonDependencies(libs, true) testImplementation(projects.libraries.matrix.test) diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt index 63fbb0bc7c..c495bd0dc4 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.matrix.api.timeline.getActiveTimeline import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import timber.log.Timber @AssistedInject class ForwardMessagesPresenter( @@ -63,7 +64,11 @@ class ForwardMessagesPresenter( roomIds: List, ) = launch { suspend { - timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).getOrThrow() + timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds) + .onFailure { + Timber.e(it, "Error while forwarding event") + } + .getOrThrow() roomIds }.runCatchingUpdatingState(forwardingActionState) } diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt index 214c795851..3e092c70fa 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt @@ -8,11 +8,13 @@ package io.element.android.features.forward.impl import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun ForwardMessagesView( @@ -24,6 +26,9 @@ fun ForwardMessagesView( onSuccess = { onForwardSuccess(it) }, + errorMessage = { + stringResource(id = CommonStrings.error_unknown) + }, onErrorDismiss = { state.eventSink(ForwardMessagesEvents.ClearError) }, From 5b18f6f93ef8113af9477ca627c3fbf10927f11e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 15:53:31 +0100 Subject: [PATCH 31/58] Code cleanup --- .../android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt | 4 ---- .../impl/pinned/DefaultPinnedEventsTimelineProvider.kt | 5 +---- .../features/messages/impl/DefaultMessagesEntryPointTest.kt | 2 +- .../roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt | 2 +- .../mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt | 2 +- .../mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt | 4 ++-- .../impl/datasource/FakeMediaGalleryDataSource.kt | 4 ++++ .../mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt | 6 +++--- .../mediaviewer/impl/viewer/MediaViewerPresenterTest.kt | 5 +++-- 9 files changed, 16 insertions(+), 18 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt index 2faed24a19..419dc074fc 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomLoadedFlowNode.kt @@ -26,7 +26,6 @@ import io.element.android.appnav.di.TimelineBindings import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.features.forward.api.ForwardEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint -import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.BackstackView @@ -45,12 +44,9 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.room.JoinedRoom -import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultPinnedEventsTimelineProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultPinnedEventsTimelineProvider.kt index 0e2de50b86..f4641c8a66 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultPinnedEventsTimelineProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/DefaultPinnedEventsTimelineProvider.kt @@ -8,9 +8,7 @@ package io.element.android.features.messages.impl.pinned import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn -import dev.zacsweers.metro.binding import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -31,8 +29,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext @SingleIn(RoomScope::class) -@ContributesBinding(RoomScope::class, binding = binding()) -@Inject +@ContributesBinding(RoomScope::class) class DefaultPinnedEventsTimelineProvider( private val room: JoinedRoom, private val syncService: SyncService, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt index 880f89f3bc..e9502346b4 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPointTest.kt @@ -90,7 +90,7 @@ class DefaultMessagesEntryPointTest { override fun navigateToRoomDetails() = lambdaError() override fun navigateToRoomMemberDetails(userId: UserId) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() - override fun forwardEvent(eventId: EventId) = lambdaError() + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() override fun navigateToRoom(roomId: RoomId) = lambdaError() } val initialTarget = MessagesEntryPoint.InitialTarget.Messages(focusedEventId = AN_EVENT_ID) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt index ebe92332e8..f9bc40b711 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPointTest.kt @@ -64,7 +64,7 @@ class DefaultRoomDetailsEntryPointTest { override fun navigateToGlobalNotificationSettings() = lambdaError() override fun navigateToRoom(roomId: RoomId, serverNames: List) = lambdaError() override fun handlePermalinkClick(data: PermalinkData, pushToBackstack: Boolean) = lambdaError() - override fun startForwardEventFlow(eventId: EventId) = lambdaError() + override fun startForwardEventFlow(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() } val params = RoomDetailsEntryPoint.Params( initialElement = RoomDetailsEntryPoint.InitialTarget.RoomDetails, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt index 189644ebd5..991e29cd98 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPointTest.kt @@ -40,7 +40,7 @@ class DefaultMediaGalleryEntryPointTest { val callback = object : MediaGalleryEntryPoint.Callback { override fun onBackClick() = lambdaError() override fun viewInTimeline(eventId: EventId) = lambdaError() - override fun forward(eventId: EventId) = lambdaError() + override fun forward(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() } val result = entryPoint.createNode( parentNode = parentNode, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt index b53fe7913c..f6246545b5 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt @@ -72,7 +72,7 @@ class DefaultMediaViewerEntryPointTest { val callback = object : MediaViewerEntryPoint.Callback { override fun onDone() = lambdaError() override fun viewInTimeline(eventId: EventId) = lambdaError() - override fun forwardEvent(eventId: EventId) = lambdaError() + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() } val params = createMediaViewerEntryPointParams() val result = entryPoint.createNode( @@ -118,7 +118,7 @@ class DefaultMediaViewerEntryPointTest { val callback = object : MediaViewerEntryPoint.Callback { override fun onDone() = lambdaError() override fun viewInTimeline(eventId: EventId) = lambdaError() - override fun forwardEvent(eventId: EventId) = lambdaError() + override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) = lambdaError() } val params = entryPoint.createParamsForAvatar( filename = "fn", diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt index ea4bdfbad8..552e6baaff 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt @@ -38,6 +38,10 @@ class FakeMediaGalleryDataSource( return groupedMediaItemsFlow.replayCache.firstOrNull() ?: AsyncData.Uninitialized } + override fun fromPinnedEvents(): Boolean { + TODO("Not yet implemented") + } + override suspend fun loadMore(direction: Timeline.PaginationDirection) { loadMoreLambda(direction) } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt index 791527445b..d223ad1ce9 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt @@ -12,15 +12,15 @@ import io.element.android.tests.testutils.lambda.lambdaError class FakeMediaViewerNavigator( private val onViewInTimelineClickLambda: (EventId) -> Unit = { lambdaError() }, - private val onForwardClickLambda: (EventId) -> Unit = { lambdaError() }, + private val onForwardClickLambda: (EventId, Boolean) -> Unit = { _, _ -> lambdaError() }, private val onItemDeletedLambda: () -> Unit = { lambdaError() }, ) : MediaViewerNavigator { override fun onViewInTimelineClick(eventId: EventId) { onViewInTimelineClickLambda(eventId) } - override fun onForwardClick(eventId: EventId) { - onForwardClickLambda(eventId) + override fun onForwardClick(eventId: EventId, fromPinnedEvents: Boolean) { + onForwardClickLambda(eventId, fromPinnedEvents) } override fun onItemDeleted() { diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt index 359eda7a5e..c0ea46b307 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt @@ -785,7 +785,7 @@ class MediaViewerPresenterTest { @Test fun `present - forward hides the bottom sheet and invokes the navigator`() = runTest { - val onForwardClickLambda = lambdaRecorder { } + val onForwardClickLambda = lambdaRecorder { _, _ -> } val navigator = FakeMediaViewerNavigator( onForwardClickLambda = onForwardClickLambda, ) @@ -804,7 +804,8 @@ class MediaViewerPresenterTest { initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID)) val finalState = awaitItem() assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) - onForwardClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID)) + onForwardClickLambda.assertions().isCalledOnce() + .with(value(AN_EVENT_ID), value(false)) } } From c99350612931ce5aec2f25174a48e1735dc95242 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 16:07:13 +0100 Subject: [PATCH 32/58] Code cleanup --- .../impl/datasource/MediaGalleryDataSource.kt | 5 ----- .../impl/viewer/MediaViewerDataSource.kt | 2 -- .../mediaviewer/impl/viewer/MediaViewerNode.kt | 14 +++++++++----- .../impl/viewer/MediaViewerPresenter.kt | 6 +++++- .../impl/viewer/SingleMediaGalleryDataSource.kt | 1 - .../impl/datasource/FakeMediaGalleryDataSource.kt | 4 ---- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt index 46bfedacb1..eb822b4969 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/MediaGalleryDataSource.kt @@ -32,7 +32,6 @@ interface MediaGalleryDataSource { fun start() fun groupedMediaItemsFlow(): Flow> fun getLastData(): AsyncData - fun fromPinnedEvents(): Boolean suspend fun loadMore(direction: Timeline.PaginationDirection) suspend fun deleteItem(eventId: EventId) } @@ -47,10 +46,6 @@ class TimelineMediaGalleryDataSource( ) : MediaGalleryDataSource { private var timeline: Timeline? = null - override fun fromPinnedEvents(): Boolean { - return timeline?.mode == Timeline.Mode.PinnedEvents - } - private val groupedMediaItemsFlow = MutableSharedFlow>(replay = 1) override fun groupedMediaItemsFlow(): Flow> = groupedMediaItemsFlow diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index 62d87a770a..1046a80fe3 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -61,8 +61,6 @@ class MediaViewerDataSource( private val localMediaStates: MutableMap>> = mutableMapOf() - fun fromPinnedEvents(): Boolean = galleryDataSource.fromPinnedEvents() - fun setup() { galleryDataSource.start() } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index 195f1589d6..f47f341657 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -81,11 +81,7 @@ class MediaViewerNode( timelineMediaGalleryDataSource } else { // Can we use a specific timeline? - val timelineMode = when (val mode = inputs.mode) { - is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> mode.timelineMode - is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> mode.timelineMode - else -> null - } + val timelineMode = inputs.mode.getTimelineMode() when (timelineMode) { null -> timelineMediaGalleryDataSource Timeline.Mode.Live, @@ -149,3 +145,11 @@ class MediaViewerNode( } } } + +internal fun MediaViewerEntryPoint.MediaViewerMode.getTimelineMode(): Timeline.Mode? { + return when (this) { + is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> timelineMode + is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> timelineMode + else -> null + } +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index be10af3f6d..a04a1a370a 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.local.LocalMedia @@ -119,7 +120,10 @@ class MediaViewerPresenter( } is MediaViewerEvents.Forward -> { mediaBottomSheetState = MediaBottomSheetState.Hidden - navigator.onForwardClick(event.eventId, fromPinnedEvents = dataSource.fromPinnedEvents()) + navigator.onForwardClick( + eventId = event.eventId, + fromPinnedEvents = inputs.mode.getTimelineMode() == Timeline.Mode.PinnedEvents, + ) } is MediaViewerEvents.OpenInfo -> coroutineScope.launch { mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt index 1d683fd8da..31964a6629 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt @@ -27,7 +27,6 @@ class SingleMediaGalleryDataSource( override fun start() = Unit override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) override fun getLastData(): AsyncData = AsyncData.Success(data) - override fun fromPinnedEvents(): Boolean = false override suspend fun loadMore(direction: Timeline.PaginationDirection) = Unit override suspend fun deleteItem(eventId: EventId) = Unit diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt index 552e6baaff..ea4bdfbad8 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/FakeMediaGalleryDataSource.kt @@ -38,10 +38,6 @@ class FakeMediaGalleryDataSource( return groupedMediaItemsFlow.replayCache.firstOrNull() ?: AsyncData.Uninitialized } - override fun fromPinnedEvents(): Boolean { - TODO("Not yet implemented") - } - override suspend fun loadMore(direction: Timeline.PaginationDirection) { loadMoreLambda(direction) } From 2eeb5fe1ccd540200b6ce30bbc3cbba9a99b5d71 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 16:08:59 +0100 Subject: [PATCH 33/58] Add test on forwarding from pinned event --- .../impl/viewer/MediaViewerPresenterTest.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt index c0ea46b307..f6de72b2d8 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt @@ -809,6 +809,33 @@ class MediaViewerPresenterTest { } } + @Test + fun `present - forward from pinned events hides the bottom sheet and invokes the navigator`() = runTest { + val onForwardClickLambda = lambdaRecorder { _, _ -> } + val navigator = FakeMediaViewerNavigator( + onForwardClickLambda = onForwardClickLambda, + ) + val presenter = createMediaViewerPresenter( + mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.PinnedEvents), + localMediaFactory = localMediaFactory, + mediaViewerNavigator = navigator, + room = FakeJoinedRoom( + baseRoom = FakeBaseRoom(canRedactOwnResult = { Result.success(true) }), + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MediaViewerEvents.OpenInfo(aMediaViewerPageData())) + val withBottomSheetState = awaitItem() + assertThat(withBottomSheetState.mediaBottomSheetState).isInstanceOf(MediaBottomSheetState.MediaDetailsBottomSheetState::class.java) + initialState.eventSink(MediaViewerEvents.Forward(AN_EVENT_ID)) + val finalState = awaitItem() + assertThat(finalState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden) + onForwardClickLambda.assertions().isCalledOnce() + .with(value(AN_EVENT_ID), value(true)) + } + } + private suspend fun ReceiveTurbine.awaitFirstItem(): T { return awaitItem() } From 5b398adfcf7d5a5aa244dba285d015e236256d35 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 3 Nov 2025 17:14:23 +0000 Subject: [PATCH 34/58] Update screenshots --- .../features.forward.impl_ForwardMessagesView_Day_3_en.png | 4 ++-- .../features.forward.impl_ForwardMessagesView_Night_3_en.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_3_en.png index c913d383ca..7866726430 100644 --- a/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c6ab936964323971e9b4927684e3ae2da96884b340569aebe6e6a711bd6c241 -size 8446 +oid sha256:9bd3fe043d7fffab7b19b5af9de7ef58ce8d2d97018a058b5b3c94cb55b277b9 +size 11273 diff --git a/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_3_en.png index 33799bf61d..ff6610c109 100644 --- a/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.forward.impl_ForwardMessagesView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd3611749f6ad60bc647236c5a10694b8ac9d1aa84e546740c1bf85df277c6f9 -size 7668 +oid sha256:97bc2d4c9621a21533da2d35abe90a5e1c1ef1bc356a6342736b61f7be8d8e3b +size 10357 From 0743b56bc4327d585cf39ab751a22ca5d69b42b6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 18:49:00 +0100 Subject: [PATCH 35/58] Always use the handleEvent(s) function the same way. --- .../android/features/call/impl/ui/CallScreenPresenter.kt | 2 +- .../android/features/forward/impl/ForwardMessagesPresenter.kt | 2 +- .../features/messages/impl/actionlist/ActionListPresenter.kt | 2 +- .../video/DefaultMediaOptimizationSelectorPresenter.kt | 2 +- .../messages/impl/messagecomposer/MessageComposerPresenter.kt | 2 +- .../features/messages/impl/timeline/TimelinePresenter.kt | 2 +- .../components/customreaction/CustomReactionPresenter.kt | 2 +- .../components/reactionsummary/ReactionSummaryPresenter.kt | 2 +- .../receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt | 2 +- .../impl/timeline/protection/TimelineProtectionPresenter.kt | 2 +- .../impl/user/editprofile/EditUserProfilePresenter.kt | 2 +- .../roomdetails/impl/members/RoomMemberListPresenter.kt | 2 +- .../notificationsettings/RoomNotificationSettingsPresenter.kt | 2 +- .../impl/rolesandpermissions/RolesAndPermissionsPresenter.kt | 2 +- .../permissions/ChangeRoomPermissionsPresenter.kt | 2 +- .../impl/RoomMemberModerationPresenter.kt | 2 +- .../io/element/android/features/share/impl/SharePresenter.kt | 2 +- .../libraries/permissions/impl/DefaultPermissionsPresenter.kt | 2 +- .../android/libraries/roomselect/impl/RoomSelectPresenter.kt | 2 +- .../libraries/voiceplayer/impl/VoiceMessagePresenter.kt | 4 ++-- 20 files changed, 21 insertions(+), 21 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index 7604dbae18..a7302c9730 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -201,7 +201,7 @@ class CallScreenPresenter( userAgent = userAgent, isCallActive = isWidgetLoaded, isInWidgetMode = isInWidgetMode, - eventSink = { handleEvents(it) }, + eventSink = ::handleEvents, ) } diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt index 63fbb0bc7c..90110b265c 100644 --- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt +++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt @@ -54,7 +54,7 @@ class ForwardMessagesPresenter( return ForwardMessagesState( forwardAction = forwardingActionState.value, - eventSink = { handleEvents(it) } + eventSink = ::handleEvents, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 192ff30f39..370b3853ea 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -122,7 +122,7 @@ class DefaultActionListPresenter( return ActionListState( target = target.value, - eventSink = { handleEvents(it) } + eventSink = ::handleEvents, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt index b62b4116e6..c0121b98b0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/video/DefaultMediaOptimizationSelectorPresenter.kt @@ -172,7 +172,7 @@ class DefaultMediaOptimizationSelectorPresenter( selectedVideoPreset = selectedVideoOptimizationPreset.dataOrNull(), displayMediaSelectorViews = displayMediaSelectorViews, displayVideoPresetSelectorDialog = displayVideoPresetSelectorDialog, - eventSink = { handleEvent(it) }, + eventSink = ::handleEvent, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 612797261a..f7dd029826 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -382,7 +382,7 @@ class MessageComposerPresenter( suggestions = suggestions.toImmutableList(), resolveMentionDisplay = resolveMentionDisplay, resolveAtRoomMentionDisplay = resolveAtRoomMentionDisplay, - eventSink = { handleEvents(it) }, + eventSink = ::handleEvents, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 8602de0532..6f9dab56c8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -289,7 +289,7 @@ class TimelinePresenter( messageShield = messageShield.value, resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, displayThreadSummaries = displayThreadSummaries, - eventSink = { handleEvents(it) } + eventSink = ::handleEvents, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt index 3d1cc93d58..bbddbb1066 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt @@ -71,7 +71,7 @@ class CustomReactionPresenter( target = target.value, selectedEmoji = selectedEmoji, recentEmojis = recentEmojis, - eventSink = { handleEvents(it) } + eventSink = ::handleEvents, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt index 4354ef5b25..c3fb43d374 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt @@ -48,7 +48,7 @@ class ReactionSummaryPresenter( } return ReactionSummaryState( target = targetWithAvatars.value, - eventSink = { handleEvents(it) } + eventSink = ::handleEvents, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt index 33316a134a..a520f1fee1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetPresenter.kt @@ -36,7 +36,7 @@ class ReadReceiptBottomSheetPresenter : Presenter { return ReadReceiptBottomSheetState( selectedEvent = selectedEvent, - eventSink = { handleEvent(it) }, + eventSink = ::handleEvent, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt index 0d9db51d98..ded49a3804 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt @@ -56,7 +56,7 @@ class TimelineProtectionPresenter( return TimelineProtectionState( protectionState = protectionState, - eventSink = { event -> handleEvent(event) } + eventSink = ::handleEvent, ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt index c894aae05d..3aa9c61555 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenter.kt @@ -143,7 +143,7 @@ class EditUserProfilePresenter( saveButtonEnabled = canSave && saveAction.value !is AsyncAction.Loading, saveAction = saveAction.value, cameraPermissionState = cameraPermissionState, - eventSink = { handleEvents(it) }, + eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 69efd04b9e..90619d2e2d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -179,7 +179,7 @@ class RoomMemberListPresenter( isSearchActive = isSearchActive, canInvite = canInvite, moderationState = roomModerationState, - eventSink = { handleEvents(it) }, + eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt index 413e71169a..7bfdf9949c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -135,7 +135,7 @@ class RoomNotificationSettingsPresenter( setNotificationSettingAction = setNotificationSettingAction.value, restoreDefaultAction = restoreDefaultAction.value, displayMentionsOnlyDisclaimer = shouldDisplayMentionsOnlyDisclaimer, - eventSink = { handleEvents(it) }, + eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt index 2ad4c84028..e7033d37be 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt @@ -100,7 +100,7 @@ class RolesAndPermissionsPresenter( canDemoteSelf = canDemoteSelf.value, changeOwnRoleAction = changeOwnRoleAction.value, resetPermissionsAction = resetPermissionsAction.value, - eventSink = { handleEvent(it) }, + eventSink = ::handleEvent, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt index ec90df1d5e..e9636978d5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsPresenter.kt @@ -112,7 +112,7 @@ class ChangeRoomPermissionsPresenter( hasChanges = hasChanges, saveAction = saveAction, confirmExitAction = confirmExitAction, - eventSink = { handleEvent(it) } + eventSink = ::handleEvent, ) } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index 2ddb3ad34a..995baa069b 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -140,7 +140,7 @@ class RoomMemberModerationPresenter( kickUserAsyncAction = kickUserAsyncAction.value, banUserAsyncAction = banUserAsyncAction.value, unbanUserAsyncAction = unbanUserAsyncAction.value, - eventSink = { handleEvent(it) }, + eventSink = ::handleEvent, ) } diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt index d3222edf5e..f5ce67498e 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt @@ -63,7 +63,7 @@ class SharePresenter( return ShareState( shareAction = shareActionState.value, - eventSink = { handleEvents(it) } + eventSink = ::handleEvents, ) } diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index f12d92a50d..c6bfbd73ed 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -125,7 +125,7 @@ class DefaultPermissionsPresenter( showDialog = showDialog.value, permissionAlreadyAsked = isAlreadyAsked, permissionAlreadyDenied = isAlreadyDenied, - eventSink = { handleEvents(it) } + eventSink = ::handleEvents, ) } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt index ffd3fda5c0..841b955018 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectPresenter.kt @@ -87,7 +87,7 @@ class RoomSelectPresenter( query = searchQuery, isSearchActive = isSearchActive, selectedRooms = selectedRooms, - eventSink = { handleEvents(it) } + eventSink = ::handleEvents, ) } } diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt index c7811f6d17..74c6aae39a 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt @@ -84,7 +84,7 @@ class VoiceMessagePresenter( } } - fun eventSink(event: VoiceMessageEvents) { + fun handleEvent(event: VoiceMessageEvents) { when (event) { is VoiceMessageEvents.PlayPause -> { if (playerState.isPlaying) { @@ -119,7 +119,7 @@ class VoiceMessagePresenter( progress = progress, time = time, showCursor = showCursor, - eventSink = { eventSink(it) }, + eventSink = ::handleEvent, ) } } From 18e46644ba41c0e52781e9a494b20de4d9bc753f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 18:53:30 +0100 Subject: [PATCH 36/58] Revert "Update notification sound" --- .../push/impl/src/main/res/raw/message.mp3 | Bin 8685 -> 22414 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/libraries/push/impl/src/main/res/raw/message.mp3 b/libraries/push/impl/src/main/res/raw/message.mp3 index abc056786c8504a9441ed74e955d9dcfa6bac927..5e9645adaf1f2b7435ef29360c690b8ddc405d3a 100644 GIT binary patch literal 22414 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

q)t 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 From 3ab67bf4216be079db3b1c9682fb621d06826eff Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 19:08:49 +0100 Subject: [PATCH 37/58] Use the new notification sound only on debug and night --- libraries/push/impl/build.gradle.kts | 4 ++++ .../push/impl/src/debug/res/raw/message.mp3 | Bin 0 -> 8685 bytes .../push/impl/src/nightly/res/raw/message.mp3 | Bin 0 -> 8685 bytes .../src/{main => release}/res/raw/message.mp3 | Bin 4 files changed, 4 insertions(+) create mode 100644 libraries/push/impl/src/debug/res/raw/message.mp3 create mode 100644 libraries/push/impl/src/nightly/res/raw/message.mp3 rename libraries/push/impl/src/{main => release}/res/raw/message.mp3 (100%) diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 87b3c681f1..b0e32481a4 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -21,6 +21,10 @@ android { isIncludeAndroidResources = true } } + + buildTypes { + register("nightly") + } } setupDependencyInjection() diff --git a/libraries/push/impl/src/debug/res/raw/message.mp3 b/libraries/push/impl/src/debug/res/raw/message.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..abc056786c8504a9441ed74e955d9dcfa6bac927 GIT binary patch literal 8685 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

q)t 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 literal 0 HcmV?d00001 diff --git a/libraries/push/impl/src/nightly/res/raw/message.mp3 b/libraries/push/impl/src/nightly/res/raw/message.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..abc056786c8504a9441ed74e955d9dcfa6bac927 GIT binary patch literal 8685 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

q)t zZ98?m7I$5NB$1MTu_RmyysMK6fvNB%!0e z4arpY?6OvfAR{6jrEwr}>3WSn6?g>`uTHuNRARP2KI8e0Vn4$uvthpzd>S&sbNL-% zfP?CtSSr~!mo_HEBVECTvxg7&ce)B2+>k@A=LhCdnvrA*EtAWycGg(C_DesXUq<@& z@5RVCPq?$uC(U7tE!w+0xLty0bO0Z$2&%)`nRfd}@1E6otx!Se+sf&|u*mYB3w(!@ zv0TKo@}U_3vvcvpk%;s`n%!<8*z6gjZr%LbU@wQ(m&9+APTge{NrgnP&N8^oZmjto zb!V`Kgx-x-CsyDNt}9EOA~1a_5D-SxP^)K`LeAfO*`5AAe>t$%4hhTiGvM^!jp>JW zN0wP%{iP(ngFTGHul!Ghf%?H=5^@%#U$9uLz&$g%a)+E zlyQTf0l6no9Q)(@FBWg*UpF;7D3eyiEQWgqyv|>Q{X1FH1cXL?ji*W_V_v!)K@;YljDQ03b>a27+WUTE`j6AkRgKT6zw!jmM6F|RKyralV(1-=wO=ei2p(p9m zb0weJ)Xt@JGXVbkC_p4NBVy)j{1P|dPa-ZTapixT2vjJeWgE?kP#OFZ3>j2?A*>Nm{uFFCICotov6v(dl2{X*M6XT&GixlQGcP8}h{40;5koV|Bz2 znA}FY`P405IlMb;uJXAl3V!g1J+O%aW=_YfXg^e4hPR+kF=+~04+ND_x={GOj!#qb z_}K)}ddn$ai$M==(Z59aVcCyRu|lBybn(Y6AO9TF^^6BqodDsjn#X_WDiRM_iGMq< z$A!Zc`}O7~kU!)i18$btEa$M?NlD_~`}TP@VJQ8;vfy6Ytd5^JK8{COhgYa;yer0#{-aa!4jtE9(Ug4rDXgW`Ef#Hi;YY>o#7N^Ijru#Ls&;dwM_>3IaD z7ZIWa9sQ;rD4K&S2$gL6^HbBYauE?bLgR?SaHJX60y}Dqq_?NWg#dC701%x}u86U6 z(jVgnQEBrMX9!PU`QHqa2vi!G$VH~Qy#BpNq6wl_CS{>^8X$@++%k%wtWw|~udzQ6 zEBrGOTDNB2noG!k>8;(M)qPR&cUksS5YF-}m$qrN$?R4-JDIn+<)pyZN1yf85I`vAK6= zI=bk$BBkw9^X9h^o40wu?FwnZ)=83WX%?Km0yFmnes~O4f9M(>ViWhc_RXWf!$JD1 zjub=Ci(ZLB#mr&RwoY@&IJz9ybAsu!kJVVD3>B!f(%X%`W}mLwR9;%D02(top6F1p zSHm#w>n8kat@CHf{u6yZ`zMt=tCv51Pd2tYE8p2-CmAJ+%P6k9&ebuNxOYB!ubvoU zg|DcSKhtWKtJpWy5gZkOStv_(TFEY;k_AXN`etr_#z}lF`TEI_*1{fLoeiUo zuH%ZRwJNL0=%0MS45#XmTPyAu-~1&Fe*2Gb%{a%fEB~EwvoM7_6V_sghuUQ2q$D6+ z9}ZZBTc2_?mrWE`j=%9-r&fR;ve{n6A_8v;sSFSr^2*zCF`>2h=wUukg#SzUXZGv@ zqd48WJ_!gAQ~0tc+c|)SEwBv7*weTMY5B+|XGhje(qjWd8XDSh&&1c(+KibgKFx!X zshbiC!;;wHDreXb1!PE;-!A;;qp;DCk%@6RPjlDa*Q!4JYbHwPj@9Seza#-bNRyQ> zuxF571vwp>!^(XYFT40Y=2Ll6dwN+(rK6eoulgZ#9nEghO{E2O;`FXmH}BRTTom*y zBXWFM2LA#$@y>tuJ~kK6Ivkq!vi*4}s zFFEg~%%Ujva-!+l?bd9*3L^BPZkrI4&Tlv0mOX3hezJEg4t@^J75m$vrVV)K(jtdHYNBwh_H+OS!#tD|ZnmA%Wx6B^t`e&3?^z$D4^h#A!gd!m=- zX&3Rlcs zDN@LzUO~sCAsB^`$ln$W+rM7l&Mdzhtc7w_Q)xN3aN5YQ<14g?3CSrklzzq{cjhgmcN(5R{XiuZ<*;y zl3kP&du?l8J-;yiZ1%jS)<&vVyS*h8*{2&K{&ji7z|(-W+M({~GqY$EgSfr%F`CiS z=~g2q7Q*}IJ}0rN+M|xR+EWynC5(fJ@4abGPQHmsjxWFE!^H}CiNiqFYHL&a`e)Pmq*fBbzYwb##GSZN6!Ej8!6qY3pmOnlCCLA$o%|Obe-s%2 literal 0 HcmV?d00001 diff --git a/libraries/push/impl/src/main/res/raw/message.mp3 b/libraries/push/impl/src/release/res/raw/message.mp3 similarity index 100% rename from libraries/push/impl/src/main/res/raw/message.mp3 rename to libraries/push/impl/src/release/res/raw/message.mp3 From 71ef154eea693297e51da4708c36f8c22bd54a67 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 3 Nov 2025 20:27:37 +0100 Subject: [PATCH 38/58] quality: fix import in test --- .../android/features/space/impl/DefaultSpaceEntryPointTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt index 696e80eeea..319d1eeeff 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/DefaultSpaceEntryPointTest.kt @@ -16,7 +16,6 @@ import io.element.android.features.space.impl.di.FakeSpaceFlowGraph import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.room.FakeJoinedRoom -import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.spaces.FakeSpaceRoomList import io.element.android.libraries.matrix.test.spaces.FakeSpaceService import io.element.android.tests.testutils.lambda.lambdaError From 7a7a6256763c7d04d14b7ae53d2344bac120b1e9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 22:18:50 +0100 Subject: [PATCH 39/58] Need to provide matchingFallbacks --- libraries/push/impl/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index b0e32481a4..091c74992f 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -23,7 +23,9 @@ android { } buildTypes { - register("nightly") + register("nightly") { + matchingFallbacks += listOf("release") + } } } From f1ad56bef1fe1b4f5fd16e42b34795eb2edcbdd7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 Nov 2025 22:29:38 +0100 Subject: [PATCH 40/58] Move dependencies block out of the android block. --- libraries/compound/build.gradle.kts | 16 ++++---- libraries/cryptography/test/build.gradle.kts | 8 ++-- libraries/dateformatter/api/build.gradle.kts | 8 ++-- libraries/dateformatter/impl/build.gradle.kts | 30 +++++++------- libraries/dateformatter/test/build.gradle.kts | 10 ++--- libraries/designsystem/build.gradle.kts | 40 +++++++++---------- libraries/featureflag/test/build.gradle.kts | 14 +++---- libraries/mediapickers/api/build.gradle.kts | 16 ++++---- libraries/mediapickers/test/build.gradle.kts | 12 +++--- libraries/preferences/test/build.gradle.kts | 16 ++++---- libraries/previewutils/build.gradle.kts | 14 +++---- libraries/ui-utils/build.gradle.kts | 14 +++---- 12 files changed, 99 insertions(+), 99 deletions(-) diff --git a/libraries/compound/build.gradle.kts b/libraries/compound/build.gradle.kts index cbdb09d451..933ef3e495 100644 --- a/libraries/compound/build.gradle.kts +++ b/libraries/compound/build.gradle.kts @@ -18,12 +18,12 @@ android { testOptions { unitTests.isIncludeAndroidResources = true } - - dependencies { - implementation(libs.showkase) - testCommonDependencies(libs) - testImplementation(libs.test.roborazzi) - testImplementation(libs.test.roborazzi.compose) - testImplementation(libs.test.roborazzi.junit) - } +} + +dependencies { + implementation(libs.showkase) + testCommonDependencies(libs) + testImplementation(libs.test.roborazzi) + testImplementation(libs.test.roborazzi.compose) + testImplementation(libs.test.roborazzi.junit) } diff --git a/libraries/cryptography/test/build.gradle.kts b/libraries/cryptography/test/build.gradle.kts index 5564c972f9..e3a110d8fb 100644 --- a/libraries/cryptography/test/build.gradle.kts +++ b/libraries/cryptography/test/build.gradle.kts @@ -11,8 +11,8 @@ plugins { android { namespace = "io.element.android.libraries.cryptography.test" - - dependencies { - api(projects.libraries.cryptography.api) - } +} + +dependencies { + api(projects.libraries.cryptography.api) } diff --git a/libraries/dateformatter/api/build.gradle.kts b/libraries/dateformatter/api/build.gradle.kts index cebb9d4049..99c22515fd 100644 --- a/libraries/dateformatter/api/build.gradle.kts +++ b/libraries/dateformatter/api/build.gradle.kts @@ -13,8 +13,8 @@ plugins { android { namespace = "io.element.android.libraries.dateformatter.api" - - dependencies { - testCommonDependencies(libs) - } +} + +dependencies { + testCommonDependencies(libs) } diff --git a/libraries/dateformatter/impl/build.gradle.kts b/libraries/dateformatter/impl/build.gradle.kts index 72da2f81f6..15c0034f91 100644 --- a/libraries/dateformatter/impl/build.gradle.kts +++ b/libraries/dateformatter/impl/build.gradle.kts @@ -30,19 +30,19 @@ android { ) } } - - dependencies { - implementation(projects.libraries.core) - implementation(projects.libraries.designsystem) - implementation(projects.libraries.di) - implementation(projects.libraries.uiStrings) - implementation(projects.services.toolbox.api) - - api(projects.libraries.dateformatter.api) - api(libs.datetime) - - testCommonDependencies(libs, true) - testImplementation(projects.libraries.dateformatter.test) - testImplementation(projects.services.toolbox.test) - } +} + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.di) + implementation(projects.libraries.uiStrings) + implementation(projects.services.toolbox.api) + + api(projects.libraries.dateformatter.api) + api(libs.datetime) + + testCommonDependencies(libs, true) + testImplementation(projects.libraries.dateformatter.test) + testImplementation(projects.services.toolbox.test) } diff --git a/libraries/dateformatter/test/build.gradle.kts b/libraries/dateformatter/test/build.gradle.kts index 6f3877ea80..af8b2a8e19 100644 --- a/libraries/dateformatter/test/build.gradle.kts +++ b/libraries/dateformatter/test/build.gradle.kts @@ -11,9 +11,9 @@ plugins { android { namespace = "io.element.android.libraries.dateformatter.test" - - dependencies { - api(projects.libraries.dateformatter.api) - api(libs.datetime) - } +} + +dependencies { + api(projects.libraries.dateformatter.api) + api(libs.datetime) } diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index ff0b4878b2..8750b04683 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -25,24 +25,24 @@ android { consumerProguardFiles("consumer-rules.pro") } } - - dependencies { - api(projects.libraries.compound) - - implementation(libs.androidx.compose.material3.windowsizeclass) - implementation(libs.androidx.compose.material3.adaptive) - implementation(libs.coil.compose) - implementation(libs.vanniktech.blurhash) - implementation(projects.libraries.androidutils) - implementation(projects.libraries.architecture) - implementation(projects.libraries.core) - implementation(projects.libraries.preferences.api) - implementation(projects.libraries.testtags) - implementation(projects.libraries.uiStrings) - - ksp(libs.showkase.processor) - implementation(libs.showkase) - - testCommonDependencies(libs) - } +} + +dependencies { + api(projects.libraries.compound) + + implementation(libs.androidx.compose.material3.windowsizeclass) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.coil.compose) + implementation(libs.vanniktech.blurhash) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.preferences.api) + implementation(projects.libraries.testtags) + implementation(projects.libraries.uiStrings) + + ksp(libs.showkase.processor) + implementation(libs.showkase) + + testCommonDependencies(libs) } diff --git a/libraries/featureflag/test/build.gradle.kts b/libraries/featureflag/test/build.gradle.kts index e2920a07b7..f2361417a0 100644 --- a/libraries/featureflag/test/build.gradle.kts +++ b/libraries/featureflag/test/build.gradle.kts @@ -11,11 +11,11 @@ plugins { android { namespace = "io.element.android.libraries.featureflag.test" - - dependencies { - api(projects.libraries.featureflag.api) - implementation(projects.libraries.core) - implementation(projects.libraries.matrix.test) - implementation(libs.coroutines.core) - } +} + +dependencies { + api(projects.libraries.featureflag.api) + implementation(projects.libraries.core) + implementation(projects.libraries.matrix.test) + implementation(libs.coroutines.core) } diff --git a/libraries/mediapickers/api/build.gradle.kts b/libraries/mediapickers/api/build.gradle.kts index c130cd7900..aaf9b4cd0c 100644 --- a/libraries/mediapickers/api/build.gradle.kts +++ b/libraries/mediapickers/api/build.gradle.kts @@ -13,12 +13,12 @@ plugins { android { namespace = "io.element.android.libraries.mediapickers.api" - - dependencies { - implementation(projects.libraries.uiStrings) - implementation(projects.libraries.core) - implementation(projects.libraries.di) - - testCommonDependencies(libs) - } +} + +dependencies { + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + + testCommonDependencies(libs) } diff --git a/libraries/mediapickers/test/build.gradle.kts b/libraries/mediapickers/test/build.gradle.kts index ee743bb63d..dc37741b02 100644 --- a/libraries/mediapickers/test/build.gradle.kts +++ b/libraries/mediapickers/test/build.gradle.kts @@ -15,10 +15,10 @@ setupDependencyInjection() android { namespace = "io.element.android.libraries.mediapickers.test" - - dependencies { - implementation(projects.libraries.core) - implementation(projects.libraries.di) - api(projects.libraries.mediapickers.api) - } +} + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.di) + api(projects.libraries.mediapickers.api) } diff --git a/libraries/preferences/test/build.gradle.kts b/libraries/preferences/test/build.gradle.kts index 9db4ab1866..44116aac13 100644 --- a/libraries/preferences/test/build.gradle.kts +++ b/libraries/preferences/test/build.gradle.kts @@ -11,12 +11,12 @@ plugins { android { namespace = "io.element.android.libraries.preferences.test" - - dependencies { - api(projects.libraries.preferences.api) - implementation(projects.libraries.matrix.api) - implementation(projects.tests.testutils) - implementation(libs.coroutines.core) - implementation(libs.androidx.datastore.preferences) - } +} + +dependencies { + api(projects.libraries.preferences.api) + implementation(projects.libraries.matrix.api) + implementation(projects.tests.testutils) + implementation(libs.coroutines.core) + implementation(libs.androidx.datastore.preferences) } diff --git a/libraries/previewutils/build.gradle.kts b/libraries/previewutils/build.gradle.kts index 92218e9286..111cb4c830 100644 --- a/libraries/previewutils/build.gradle.kts +++ b/libraries/previewutils/build.gradle.kts @@ -11,11 +11,11 @@ plugins { android { namespace = "io.element.android.libraries.previewutils" - - dependencies { - implementation(projects.libraries.designsystem) - implementation(projects.libraries.matrix.api) - - implementation(libs.kotlinx.collections.immutable) - } +} + +dependencies { + implementation(projects.libraries.designsystem) + implementation(projects.libraries.matrix.api) + + implementation(libs.kotlinx.collections.immutable) } diff --git a/libraries/ui-utils/build.gradle.kts b/libraries/ui-utils/build.gradle.kts index 95ce3d21a1..227bdd7f49 100644 --- a/libraries/ui-utils/build.gradle.kts +++ b/libraries/ui-utils/build.gradle.kts @@ -13,11 +13,11 @@ plugins { android { namespace = "io.element.android.libraries.ui.utils" - - dependencies { - implementation(projects.libraries.androidutils) - implementation(projects.services.toolbox.impl) - - testCommonDependencies(libs) - } +} + +dependencies { + implementation(projects.libraries.androidutils) + implementation(projects.services.toolbox.impl) + + testCommonDependencies(libs) } From 1e517252e156a28ae06df172359e53462ac97cca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 08:23:34 +0000 Subject: [PATCH 41/58] fix(deps): update roborazzi to v1.51.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7f07c14167..cbf32ec299 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ accompanist = "0.37.3" # Test test_core = "1.7.0" -roborazzi = "1.50.0" +roborazzi = "1.51.0" # Jetbrain datetime = "0.7.1" From 8d529849e88441ffcd83b7a28d66ea6953f49be8 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 4 Nov 2025 11:36:17 +0100 Subject: [PATCH 42/58] Try fixing 'Timeline Event object has already been destroyed' (#5675) This will display a fallback notification. I don't see how the current code could cause it, but I tried to re-structure it a bit so we don't have nested `use` usages and `timestamp` is fetched ahead of time. --- .../libraries/matrix/impl/notification/NotificationMapper.kt | 5 +++-- .../notification/TimelineEventToNotificationContentMapper.kt | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt index bad622e456..7e06eb3c75 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt @@ -39,6 +39,7 @@ class NotificationMapper( isDirect = item.roomInfo.isDirect, activeMembersCount = item.roomInfo.joinedMembersCount.toInt(), ) + val timestamp = item.timestamp() ?: clock.epochMillis() NotificationData( sessionId = sessionId, eventId = eventId, @@ -53,8 +54,8 @@ class NotificationMapper( isDm = isDm, isEncrypted = item.roomInfo.isEncrypted.orFalse(), isNoisy = item.isNoisy.orFalse(), - timestamp = item.timestamp() ?: clock.epochMillis(), - content = item.event.use { notificationContentMapper.map(it) }.getOrThrow(), + timestamp = timestamp, + content = notificationContentMapper.map(item.event).getOrThrow(), hasMention = item.hasMention.orFalse(), ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt index 2ca4a3c823..b2952461df 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventToNotificationContentMapper.kt @@ -25,8 +25,9 @@ class TimelineEventToNotificationContentMapper { fun map(timelineEvent: TimelineEvent): Result { return runCatchingExceptions { timelineEvent.use { + val senderId = UserId(timelineEvent.senderId()) timelineEvent.eventType().use { eventType -> - eventType.toContent(senderId = UserId(timelineEvent.senderId())) + eventType.toContent(senderId = senderId) } } } From a2b6561009008d3aaa2253ac006814c7fcdc185c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 Nov 2025 12:11:11 +0100 Subject: [PATCH 43/58] Make sure we know the session verification state before showing the option to verify the session. #5521 --- .../ChooseSelfVerificationModePresenter.kt | 34 ++++++- .../ChooseSelfVerificationModeState.kt | 11 ++- ...ChooseSelfVerificationModeStateProvider.kt | 42 +++++++-- .../ChooseSelfVerificationModeView.kt | 78 ++++++++++----- ...oseSessionVerificationModePresenterTest.kt | 94 ++++++++++++++++--- .../ChooseSessionVerificationModeViewTest.kt | 5 +- .../api/encryption/EncryptionService.kt | 3 +- .../impl/encryption/RustEncryptionService.kt | 16 +++- .../test/encryption/FakeEncryptionService.kt | 5 +- 9 files changed, 228 insertions(+), 60 deletions(-) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt index eb3c1330b3..32419a0a59 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt @@ -15,7 +15,9 @@ import androidx.compose.runtime.remember import dev.zacsweers.metro.Inject import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState @@ -27,8 +29,33 @@ class ChooseSelfVerificationModePresenter( @Composable override fun present(): ChooseSelfVerificationModeState { val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState() - val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() - val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } } + val canEnterRecoveryKey by encryptionService.recoveryStateStateFlow + .mapState { recoveryState -> + when (recoveryState) { + RecoveryState.WAITING_FOR_SYNC, + RecoveryState.UNKNOWN -> AsyncData.Loading() + RecoveryState.INCOMPLETE -> AsyncData.Success(true) + RecoveryState.ENABLED, + RecoveryState.DISABLED -> AsyncData.Success(false) + } + } + .collectAsState() + val buttonsState by remember { + derivedStateOf { + val canUseAnotherDevice = hasDevicesToVerifyAgainst.dataOrNull() + val canEnterRecoveryKey = canEnterRecoveryKey.dataOrNull() + if (canUseAnotherDevice == null || canEnterRecoveryKey == null) { + AsyncData.Loading() + } else { + AsyncData.Success( + ChooseSelfVerificationModeState.ButtonsState( + canUseAnotherDevice = canUseAnotherDevice, + canEnterRecoveryKey = canEnterRecoveryKey, + ) + ) + } + } + } val directLogoutState = directLogoutPresenter.present() @@ -39,8 +66,7 @@ class ChooseSelfVerificationModePresenter( } return ChooseSelfVerificationModeState( - canUseAnotherDevice = hasDevicesToVerifyAgainst, - canEnterRecoveryKey = canEnterRecoveryKey, + buttonsState = buttonsState, directLogoutState = directLogoutState, eventSink = ::eventHandler, ) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt index 117768a6d2..5cc03352f0 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeState.kt @@ -8,10 +8,15 @@ package io.element.android.features.ftue.impl.sessionverification.choosemode import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.libraries.architecture.AsyncData data class ChooseSelfVerificationModeState( - val canUseAnotherDevice: Boolean, - val canEnterRecoveryKey: Boolean, + val buttonsState: AsyncData, val directLogoutState: DirectLogoutState, val eventSink: (ChooseSelfVerificationModeEvent) -> Unit, -) +) { + data class ButtonsState( + val canUseAnotherDevice: Boolean, + val canEnterRecoveryKey: Boolean, + ) +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt index e053728e2c..fa480706fd 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt @@ -9,23 +9,49 @@ package io.element.android.features.ftue.impl.sessionverification.choosemode import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.libraries.architecture.AsyncData class ChooseSelfVerificationModeStateProvider : PreviewParameterProvider { override val values = sequenceOf( - aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = true), - aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = false), - aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = true), - aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = false), + aChooseSelfVerificationModeState( + buttonsState = AsyncData.Success( + aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = true), + ), + ), + aChooseSelfVerificationModeState( + buttonsState = AsyncData.Success( + aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = false), + ), + ), + aChooseSelfVerificationModeState( + buttonsState = AsyncData.Success( + aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = true), + ), + ), + aChooseSelfVerificationModeState( + buttonsState = AsyncData.Success( + aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = false), + ), + ), + aChooseSelfVerificationModeState( + buttonsState = AsyncData.Loading(), + ), ) } fun aChooseSelfVerificationModeState( - canUseAnotherDevice: Boolean = true, - canEnterRecoveryKey: Boolean = true, + buttonsState: AsyncData = AsyncData.Success(aButtonsState()), ) = ChooseSelfVerificationModeState( - canUseAnotherDevice = canUseAnotherDevice, - canEnterRecoveryKey = canEnterRecoveryKey, + buttonsState = buttonsState, directLogoutState = aDirectLogoutState(), eventSink = {}, ) + +fun aButtonsState( + canUseAnotherDevice: Boolean = true, + canEnterRecoveryKey: Boolean = true, +) = ChooseSelfVerificationModeState.ButtonsState( + canUseAnotherDevice = canUseAnotherDevice, + canEnterRecoveryKey = canEnterRecoveryKey, +) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt index b07c04ac9c..6907414863 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.ftue.impl.R +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage @@ -50,7 +51,6 @@ fun ChooseSelfVerificationModeView( BackHandler { activity?.finish() } - HeaderFooterPage( modifier = modifier, topBar = { @@ -73,29 +73,12 @@ fun ChooseSelfVerificationModeView( ) }, footer = { - ButtonColumnMolecule( - modifier = Modifier.padding(bottom = 16.dp) - ) { - if (state.canUseAnotherDevice) { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_use_another_device), - onClick = onUseAnotherDevice, - ) - } - if (state.canEnterRecoveryKey) { - Button( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_session_verification_enter_recovery_key), - onClick = onUseRecoveryKey, - ) - } - OutlinedButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), - onClick = onResetKey, - ) - } + ChooseSelfVerificationModeButtons( + state = state, + onUseAnotherDevice = onUseAnotherDevice, + onUseRecoveryKey = onUseRecoveryKey, + onResetKey = onResetKey, + ) } ) { Row( @@ -113,6 +96,53 @@ fun ChooseSelfVerificationModeView( } } +@Composable +private fun ChooseSelfVerificationModeButtons( + state: ChooseSelfVerificationModeState, + onUseAnotherDevice: () -> Unit, + onUseRecoveryKey: () -> Unit, + onResetKey: () -> Unit, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 16.dp) + ) { + when (state.buttonsState) { + AsyncData.Uninitialized, + is AsyncData.Failure, + is AsyncData.Loading -> { + Button( + modifier = Modifier.fillMaxWidth(), + enabled = false, + showProgress = true, + text = stringResource(CommonStrings.common_loading), + onClick = {}, + ) + } + is AsyncData.Success -> { + if (state.buttonsState.data.canUseAnotherDevice) { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_use_another_device), + onClick = onUseAnotherDevice, + ) + } + if (state.buttonsState.data.canEnterRecoveryKey) { + Button( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_session_verification_enter_recovery_key), + onClick = onUseRecoveryKey, + ) + } + OutlinedButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), + onClick = onResetKey, + ) + } + } + } +} + @PreviewsDayNight @Composable internal fun ChooseSelfVerificationModeViewPreview( diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt index 3dbf5a6932..2caead346a 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt @@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService @@ -22,23 +23,92 @@ import org.junit.Test class ChooseSessionVerificationModePresenterTest { @Test - fun `initial state - is relayed from EncryptionService`() = runTest { - val encryptionService = FakeEncryptionService().apply { - // Has device to verify against - emitHasDevicesToVerifyAgainst(false) - // Can enter recovery key - emitRecoveryState(RecoveryState.INCOMPLETE) - } - val presenter = createPresenter(encryptionService = encryptionService) + fun `present - initial state`() = runTest { + val presenter = createPresenter() presenter.test { awaitItem().run { - assertThat(canUseAnotherDevice).isFalse() - assertThat(canEnterRecoveryKey).isTrue() + assertThat(buttonsState.isLoading()).isTrue() assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue() } } } + @Test + fun `present - state is relayed from EncryptionService, order 1`() = runTest { + val encryptionService = FakeEncryptionService() + val presenter = createPresenter(encryptionService = encryptionService) + presenter.test { + assertThat(awaitItem().buttonsState.isLoading()).isTrue() + // Has device to verify against + encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false)) + // Can enter recovery key + encryptionService.emitRecoveryState(RecoveryState.DISABLED) + assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo( + ChooseSelfVerificationModeState.ButtonsState( + canUseAnotherDevice = false, + canEnterRecoveryKey = false, + ) + ) + } + } + + @Test + fun `present - state is relayed from EncryptionService, order 2`() = runTest { + val encryptionService = FakeEncryptionService() + val presenter = createPresenter(encryptionService = encryptionService) + presenter.test { + assertThat(awaitItem().buttonsState.isLoading()).isTrue() + // Can enter recovery key + encryptionService.emitRecoveryState(RecoveryState.DISABLED) + // Has device to verify against + encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false)) + assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo( + ChooseSelfVerificationModeState.ButtonsState( + canUseAnotherDevice = false, + canEnterRecoveryKey = false, + ) + ) + } + } + + @Test + fun `present - can use another device`() = runTest { + val encryptionService = FakeEncryptionService() + val presenter = createPresenter(encryptionService = encryptionService) + presenter.test { + assertThat(awaitItem().buttonsState.isLoading()).isTrue() + // Can enter recovery key + encryptionService.emitRecoveryState(RecoveryState.DISABLED) + // Has device to verify against + encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(true)) + assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo( + ChooseSelfVerificationModeState.ButtonsState( + canUseAnotherDevice = true, + canEnterRecoveryKey = false, + ) + ) + } + } + + @Test + fun `present - can enter recovery key`() = runTest { + val encryptionService = FakeEncryptionService() + val presenter = createPresenter(encryptionService = encryptionService) + presenter.test { + assertThat(awaitItem().buttonsState.isLoading()).isTrue() + // Can enter recovery key + encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) + // Has device to verify against + encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false)) + assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo( + ChooseSelfVerificationModeState.ButtonsState( + canUseAnotherDevice = false, + canEnterRecoveryKey = true, + ) + ) + } + } + @Test fun `sing out action triggers a direct logout`() = runTest { val logoutEventRecorder = lambdaRecorder {} @@ -49,8 +119,8 @@ class ChooseSessionVerificationModePresenterTest { presenter.test { val initial = awaitItem() initial.eventSink(ChooseSelfVerificationModeEvent.SignOut) - - logoutEventRecorder.assertions().isCalledOnce().with(value(DirectLogoutEvents.Logout(ignoreSdkError = false))) + logoutEventRecorder.assertions().isCalledOnce() + .with(value(DirectLogoutEvents.Logout(ignoreSdkError = false))) } } diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt index ed7d99dd19..3112a7af59 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModeViewTest.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.ftue.impl.R +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.clickOn @@ -43,7 +44,7 @@ class ChooseSessionVerificationModeViewTest { fun `clicking on use another device calls the callback`() { ensureCalledOnce { callback -> rule.setChooseSelfVerificationModeView( - aChooseSelfVerificationModeState(canUseAnotherDevice = true), + aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))), onUseAnotherDevice = callback, ) rule.clickOn(R.string.screen_identity_use_another_device) @@ -55,7 +56,7 @@ class ChooseSessionVerificationModeViewTest { fun `clicking on enter recovery key calls the callback`() { ensureCalledOnce { callback -> rule.setChooseSelfVerificationModeView( - aChooseSelfVerificationModeState(canEnterRecoveryKey = true), + aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canEnterRecoveryKey = true))), onEnterRecoveryKey = callback, ) rule.clickOn(R.string.screen_session_verification_enter_recovery_key) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index 961178ee3e..22059ad155 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.api.encryption +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import kotlinx.coroutines.flow.Flow @@ -17,7 +18,7 @@ interface EncryptionService { val recoveryStateStateFlow: StateFlow val enableRecoveryProgressStateFlow: StateFlow val isLastDevice: StateFlow - val hasDevicesToVerifyAgainst: StateFlow + val hasDevicesToVerifyAgainst: StateFlow> suspend fun enableBackups(): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index 91cafd1df0..8e22827063 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.encryption +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.extensions.mapFailure @@ -42,6 +43,7 @@ import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener import org.matrix.rustcomponents.sdk.Encryption import org.matrix.rustcomponents.sdk.UserIdentity +import timber.log.Timber import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress import org.matrix.rustcomponents.sdk.RecoveryException as RustRecoveryException @@ -103,14 +105,20 @@ class RustEncryptionService( * TODO This is a temporary workaround, when we will have a way to observe * the sessions, this code will have to be updated. */ - override val hasDevicesToVerifyAgainst: StateFlow = flow { + override val hasDevicesToVerifyAgainst: StateFlow> = flow { while (currentCoroutineContext().isActive) { - val result = hasDevicesToVerifyAgainst().getOrDefault(false) - emit(result) + val result = hasDevicesToVerifyAgainst() + result + .onSuccess { + emit(AsyncData.Success(it)) + } + .onFailure { + Timber.e(it, "Failed to get hasDevicesToVerifyAgainst, retrying in 5s...") + } delay(5_000) } } - .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) + .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, AsyncData.Uninitialized) override suspend fun enableBackups(): Result = withContext(dispatchers.io) { runCatchingExceptions { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index b4199ff8e4..5b2ead9acf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.test.encryption +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState @@ -34,7 +35,7 @@ class FakeEncryptionService( override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Starting) override val isLastDevice: MutableStateFlow = MutableStateFlow(false) - override val hasDevicesToVerifyAgainst: MutableStateFlow = MutableStateFlow(true) + override val hasDevicesToVerifyAgainst: MutableStateFlow> = MutableStateFlow(AsyncData.Uninitialized) private var waitForBackupUploadSteadyStateFlow: Flow = flowOf() private var recoverFailure: Exception? = null @@ -84,7 +85,7 @@ class FakeEncryptionService( this.isLastDevice.value = isLastDevice } - fun emitHasDevicesToVerifyAgainst(hasDevicesToVerifyAgainst: Boolean) { + fun emitHasDevicesToVerifyAgainst(hasDevicesToVerifyAgainst: AsyncData) { this.hasDevicesToVerifyAgainst.value = hasDevicesToVerifyAgainst } From 90704208601b5afff4f49a5d66b8b7ffdfd06c5a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 Nov 2025 15:25:13 +0100 Subject: [PATCH 44/58] Session database: add count query. --- .../android/libraries/sessionstorage/api/SessionStore.kt | 5 +++++ .../sessionstorage/impl/DatabaseSessionStore.kt | 9 +++++++++ .../android/libraries/matrix/session/SessionData.sq | 3 +++ .../sessionstorage/impl/DatabaseSessionStoreTest.kt | 6 ++++++ .../sessionstorage/test/InMemorySessionStore.kt | 4 ++++ 5 files changed, 27 insertions(+) diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index 7900b4d90d..f3b66d73c0 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -50,6 +50,11 @@ interface SessionStore { */ suspend fun getAllSessions(): List + /** + * Get the number of sessions. + */ + suspend fun numberOfSessions(): Int + /** * Get the latest session, or null if no session exists. */ diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index 0560d3076a..81353f5305 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -161,6 +161,15 @@ class DatabaseSessionStore( } } + override suspend fun numberOfSessions(): Int { + return sessionDataMutex.withLock { + database.sessionDataQueries.count() + .executeAsOneOrNull() + ?.toInt() + ?: 0 + } + } + override fun sessionsFlow(): Flow> { return database.sessionDataQueries.selectAll() .asFlow() diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index 53d07bfba3..b61c746fb8 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -47,6 +47,9 @@ SELECT * FROM SessionData ORDER BY lastUsageIndex DESC LIMIT 1; selectAll: SELECT * FROM SessionData ORDER BY lastUsageIndex DESC; +count: +SELECT count(*) FROM SessionData; + selectByUserId: SELECT * FROM SessionData WHERE userId = ?; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt index d1fef8a39a..f28d9e21df 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTest.kt @@ -52,6 +52,7 @@ class DatabaseSessionStoreTest { assertThat(database.sessionDataQueries.selectLatest().executeAsOneOrNull()).isEqualTo(aSessionData) assertThat(database.sessionDataQueries.selectAll().executeAsList().size).isEqualTo(1) + assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(1) } @Test @@ -109,6 +110,7 @@ class DatabaseSessionStoreTest { assertThat(foundSession).isEqualTo(aSessionData) assertThat(database.sessionDataQueries.selectAll().executeAsList().size).isEqualTo(2) + assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(2) } @Test @@ -196,12 +198,16 @@ class DatabaseSessionStoreTest { position = 1, lastUsageIndex = 1, ) + assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(1) databaseSessionStore.addSession(secondSessionData.toApiModel()) assertThat(awaitItem().size).isEqualTo(2) + assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(2) databaseSessionStore.removeSession(aSessionData.userId) assertThat(awaitItem().size).isEqualTo(1) + assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(1) databaseSessionStore.removeSession(secondSessionData.userId) assertThat(awaitItem()).isEmpty() + assertThat(database.sessionDataQueries.count().executeAsOneOrNull()).isEqualTo(0) } } diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt index c8f3078e7a..00dd9ad9c0 100644 --- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/InMemorySessionStore.kt @@ -67,6 +67,10 @@ class InMemorySessionStore( return sessionDataListFlow.value } + override suspend fun numberOfSessions(): Int { + return sessionDataListFlow.value.size + } + override suspend fun getLatestSession(): SessionData? { return sessionDataListFlow.value.firstOrNull() } From 604d091dbccb7e727e38fb1fb3bd5213584513cd Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 4 Nov 2025 14:28:16 +0000 Subject: [PATCH 45/58] Update screenshots --- ...tion.choosemode_ChooseSelfVerificationModeView_Day_4_en.png | 3 +++ ...on.choosemode_ChooseSelfVerificationModeView_Night_4_en.png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en.png new file mode 100644 index 0000000000..7bf8eee433 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd6ddafdb4bf56b2d79958fbff2cc6afb384d612bc3850a0fd4c0f4e9368abea +size 23346 diff --git a/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en.png new file mode 100644 index 0000000000..3ff036987d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.ftue.impl.sessionverification.choosemode_ChooseSelfVerificationModeView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cf3c48bc7e54774134d819053e2e63d7e0a0b053c2364d54a39452d3c77b794 +size 22896 From 0367ae610cfede75081de620802bd66736d844b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 Nov 2025 15:30:29 +0100 Subject: [PATCH 46/58] Session database: use the new count API. --- .../src/main/kotlin/io/element/android/appnav/RootFlowNode.kt | 4 ++-- .../login/impl/screens/onboarding/OnBoardingPresenter.kt | 2 +- .../features/rageshake/impl/reporter/DefaultBugReporter.kt | 2 +- .../android/features/signedout/impl/SignedOutPresenterTest.kt | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index bdad3c2ab0..0332f34306 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -349,7 +349,7 @@ class RootFlowNode( } else { // wait for the current session to be restored val loggedInFlowNode = attachSession(latestSessionId) - if (sessionStore.getAllSessions().size > 1) { + if (sessionStore.numberOfSessions() > 1) { // Several accounts, let the user choose which one to use backstack.push( NavTarget.AccountSelect( @@ -379,7 +379,7 @@ class RootFlowNode( is PermalinkData.FallbackLink -> Unit is PermalinkData.RoomEmailInviteLink -> Unit else -> { - if (sessionStore.getAllSessions().size > 1) { + if (sessionStore.numberOfSessions() > 1) { // Several accounts, let the user choose which one to use backstack.push( NavTarget.AccountSelect( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index e7e20aa70d..44d8095fa3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -90,7 +90,7 @@ class OnBoardingPresenter( } val isAddingAccount by produceState(initialValue = false) { // We are adding an account if there is at least one session already stored - value = sessionStore.getAllSessions().isNotEmpty() + value = sessionStore.numberOfSessions() > 0 } val loginMode by loginHelper.collectLoginMode() diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 89bec00ac5..7f173a8fe1 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -165,7 +165,7 @@ class DefaultBugReporter( } } val sessionData = sessionStore.getLatestSession() - val numberOfAccounts = sessionStore.getAllSessions().size + val numberOfAccounts = sessionStore.numberOfSessions() val deviceId = sessionData?.deviceId ?: "undefined" val userId = sessionData?.userId?.let { UserId(it) } // build the multi part request diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt index e53c0af112..d5a9a910a0 100644 --- a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -58,9 +58,11 @@ class SignedOutPresenterTest { val initialState = awaitItem() assertThat(initialState.signedOutSession).isEqualTo(aSessionData) assertThat(sessionStore.getAllSessions()).isNotEmpty() + assertThat(sessionStore.numberOfSessions()).isEqualTo(1) initialState.eventSink(SignedOutEvents.SignInAgain) assertThat(awaitItem().signedOutSession).isNull() assertThat(sessionStore.getAllSessions()).isEmpty() + assertThat(sessionStore.numberOfSessions()).isEqualTo(0) } } } From 015b497d5afd04e9460138f346a54fb51a7d48bf Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 4 Nov 2025 15:43:00 +0100 Subject: [PATCH 47/58] Use the SDK Client to check whether a homeserver is compatible (#5664) * Use the SDK `Client` to check whether a HS is compatible * Remove usage of unused `WellKnown`, keep `ElementWellKnown` * Make `HomeServerLoginCompatibilityChecker.check` return `true/false` values to distinguish non-valid homeservers from a failed check * Use `inMemoryStore` and `serverNameOrHomeserverUrl` * Do some cleanup of `isValid` and `isWellknownValid` * Make the debounce for starting the search a bit higher, as checking for the homeservers seems more resource-intensive now --- .../impl/accountprovider/AccountProvider.kt | 1 - .../AccountProviderProvider.kt | 4 +- .../login/impl/resolver/HomeserverData.kt | 2 - .../login/impl/resolver/HomeserverResolver.kt | 42 ++---- .../ChangeAccountProviderPresenter.kt | 1 - .../ChooseAccountProviderPresenter.kt | 1 - .../SearchAccountProviderPresenter.kt | 4 +- .../SearchAccountProviderStateProvider.kt | 12 +- .../SearchAccountProviderView.kt | 1 - .../AccountProviderDataSourceTest.kt | 4 - .../ChangeAccountProviderPresenterTest.kt | 4 - .../ChooseAccountProviderPresenterTest.kt | 2 - .../SearchAccountProviderPresenterTest.kt | 111 ++++++-------- .../HomeServerLoginCompatibilityChecker.kt | 19 +++ ...RustHomeServerLoginCompatibilityChecker.kt | 36 +++++ ...HomeserverLoginCompatibilityCheckerTest.kt | 54 +++++++ .../fakes/FakeFfiHomeserverLoginDetails.kt | 6 +- ...FakeHomeServerLoginCompatibilityChecker.kt | 18 +++ .../api/SessionWellknownRetriever.kt | 1 - .../libraries/wellknown/api/WellKnown.kt | 17 --- .../wellknown/api/WellknownRetriever.kt | 1 - .../impl/DefaultSessionWellknownRetriever.kt | 12 -- .../impl/DefaultWellknownRetriever.kt | 22 --- .../libraries/wellknown/impl/Mapper.kt | 11 -- .../DefaultSessionWellknownRetrieverTest.kt | 138 ------------------ .../test/FakeSessionWellknownRetriever.kt | 6 - .../wellknown/test/FakeWellknownRetriever.kt | 6 - 27 files changed, 195 insertions(+), 341 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/HomeServerLoginCompatibilityChecker.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeHomeServerLoginCompatibilityChecker.kt delete mode 100644 libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt index 0fcef6bedf..2d9c2d2e9c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt @@ -13,5 +13,4 @@ data class AccountProvider( val subtitle: String? = null, val isPublic: Boolean = false, val isMatrixOrg: Boolean = false, - val isValid: Boolean = false, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt index a5f0fd7d3b..886f0efb82 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt @@ -15,7 +15,7 @@ open class AccountProviderProvider : PreviewParameterProvider { get() = sequenceOf( anAccountProvider(), anAccountProvider().copy(subtitle = null), - anAccountProvider().copy(subtitle = null, title = "invalid", isValid = false), + anAccountProvider().copy(subtitle = null, title = "invalid"), anAccountProvider().copy(subtitle = null, title = "Other", isPublic = false, isMatrixOrg = false), // Add other state here ) @@ -26,11 +26,9 @@ fun anAccountProvider( subtitle: String? = "Matrix.org is an open network for secure, decentralized communication.", isPublic: Boolean = true, isMatrixOrg: Boolean = true, - isValid: Boolean = true, ) = AccountProvider( url = url, subtitle = subtitle, isPublic = isPublic, isMatrixOrg = isMatrixOrg, - isValid = isValid, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt index 0b4b088938..1e219a8a3b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt @@ -10,6 +10,4 @@ package io.element.android.features.login.impl.resolver data class HomeserverData( // The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url val homeserverUrl: String, - // True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid - val isWellknownValid: Boolean, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt index 7b6f3e4102..c43839517c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt @@ -8,19 +8,16 @@ package io.element.android.features.login.impl.resolver import dev.zacsweers.metro.Inject -import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.parallelMap -import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl -import io.element.android.libraries.wellknown.api.WellKnown -import io.element.android.libraries.wellknown.api.WellknownRetriever +import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout +import timber.log.Timber import java.util.Collections /** @@ -29,7 +26,7 @@ import java.util.Collections @Inject class HomeserverResolver( private val dispatchers: CoroutineDispatchers, - private val wellknownRetriever: WellknownRetriever, + private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, ) { fun resolve(userInput: String): Flow> = flow { val flowContext = currentCoroutineContext() @@ -41,20 +38,14 @@ class HomeserverResolver( // Run all the requests in parallel withContext(dispatchers.io) { list.parallelMap { url -> - val wellKnown = tryOrNull { - withTimeout(5000) { - wellknownRetriever.getWellKnown(url) - } - } - val isValid = wellKnown?.dataOrNull()?.isValid().orFalse() + val isValid = homeServerLoginCompatibilityChecker.check(url) + .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } + .getOrNull() + ?: return@parallelMap + + // Emit the list as soon as possible if (isValid) { - // Emit the list as soon as possible - currentList.add( - HomeserverData( - homeserverUrl = url, - isWellknownValid = true, - ) - ) + currentList.add(HomeserverData(homeserverUrl = url)) withContext(flowContext) { emit(currentList.toList()) } @@ -63,14 +54,7 @@ class HomeserverResolver( } // If list is empty, and the user has entered an URL, do not block the user. if (currentList.isEmpty() && trimmedUserInput.isValidUrl()) { - emit( - listOf( - HomeserverData( - homeserverUrl = trimmedUserInput, - isWellknownValid = false, - ) - ) - ) + emit(listOf(HomeserverData(homeserverUrl = trimmedUserInput))) } } @@ -88,7 +72,3 @@ class HomeserverResolver( } } } - -private fun WellKnown.isValid(): Boolean { - return homeServer?.baseURL?.isNotBlank().orFalse() -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt index 8e6a3ef0ba..940889728e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -37,7 +37,6 @@ class ChangeAccountProviderPresenter( subtitle = null, isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, - isValid = true, ) } .toImmutableList() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt index 73f03ba7c8..0c915959cb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -67,7 +67,6 @@ class ChooseAccountProviderPresenter( subtitle = null, isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, - isValid = true, ) } .toImmutableList() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt index 0aa06ca632..657a21111c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt @@ -57,14 +57,14 @@ class SearchAccountProviderPresenter( userInput = userInput, userInputResult = data.value, changeServerState = changeServerState, - eventSink = ::handleEvents + eventSink = ::handleEvents, ) } private fun CoroutineScope.onUserInput(userInput: String, data: MutableState>>) = launch { data.value = AsyncData.Uninitialized // Debounce - delay(300) + delay(500) data.value = AsyncData.Loading() homeserverResolver.resolve(userInput).collect { data.value = AsyncData.Success(it) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt index fb0e0f5c5a..3dd7a3d8c5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt @@ -34,18 +34,14 @@ fun aSearchAccountProviderState( fun aHomeserverDataList(): List { return listOf( - aHomeserverData(isWellknownValid = true), - aHomeserverData(homeserverUrl = "https://no.sliding.sync", isWellknownValid = true), - aHomeserverData(homeserverUrl = "https://invalid", isWellknownValid = false), + aHomeserverData(homeserverUrl = AuthenticationConfig.MATRIX_ORG_URL), + aHomeserverData(homeserverUrl = "https://no.sliding.sync"), + aHomeserverData(homeserverUrl = "https://invalid"), ) } fun aHomeserverData( homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL, - isWellknownValid: Boolean = true, ): HomeserverData { - return HomeserverData( - homeserverUrl = homeserverUrl, - isWellknownValid = isWellknownValid, - ) + return HomeserverData(homeserverUrl = homeserverUrl,) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index 13bfd9e38e..2b289aa4c1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt @@ -192,7 +192,6 @@ private fun HomeserverData.toAccountProvider(): AccountProvider { // There is no need to know for other servers right now isPublic = isMatrixOrg, isMatrixOrg = isMatrixOrg, - isValid = isWellknownValid, ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt index 3d84a1da41..8a8f6864cf 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt @@ -33,7 +33,6 @@ class AccountProviderDataSourceTest { subtitle = null, isPublic = true, isMatrixOrg = true, - isValid = false, ) ) } @@ -55,7 +54,6 @@ class AccountProviderDataSourceTest { subtitle = null, isPublic = true, isMatrixOrg = true, - isValid = false, ) ) } @@ -77,7 +75,6 @@ class AccountProviderDataSourceTest { subtitle = null, isPublic = true, isMatrixOrg = true, - isValid = false, ) ) } @@ -98,7 +95,6 @@ class AccountProviderDataSourceTest { subtitle = null, isPublic = false, isMatrixOrg = false, - isValid = false, ) ) sut.reset() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index 89abc6ddef..f2e933390b 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -46,7 +46,6 @@ class ChangeAccountProviderPresenterTest { subtitle = null, isPublic = true, isMatrixOrg = true, - isValid = true, ) ) ) @@ -76,7 +75,6 @@ class ChangeAccountProviderPresenterTest { subtitle = null, isPublic = true, isMatrixOrg = true, - isValid = true, ), AccountProvider( url = "https://element.io", @@ -84,7 +82,6 @@ class ChangeAccountProviderPresenterTest { subtitle = null, isPublic = false, isMatrixOrg = false, - isValid = true, ) ) ) @@ -114,7 +111,6 @@ class ChangeAccountProviderPresenterTest { subtitle = null, isPublic = true, isMatrixOrg = true, - isValid = true, ) ) ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt index 5bad8a3638..2e13b0e555 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt @@ -37,14 +37,12 @@ class ChooseAccountProviderPresenterTest { subtitle = null, isPublic = false, isMatrixOrg = false, - isValid = true, ) val accountProvider2 = AccountProvider( url = ACCOUNT_PROVIDER_FROM_CONFIG_2.ensureProtocol(), subtitle = null, isPublic = false, isMatrixOrg = false, - isValid = true, ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt index 79cd3c954a..67453119c7 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt @@ -13,12 +13,8 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverResolver -import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.test.A_HOMESERVER_URL -import io.element.android.libraries.wellknown.api.WellKnown -import io.element.android.libraries.wellknown.api.WellKnownBaseConfig -import io.element.android.libraries.wellknown.api.WellknownRetrieverResult +import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -33,9 +29,9 @@ class SearchAccountProviderPresenterTest { @Test fun `present - initial state`() = runTest { - val fakeWellknownRetriever = FakeWellknownRetriever() + val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { @@ -47,9 +43,35 @@ class SearchAccountProviderPresenterTest { } } + @Test + fun `present - error while checking login compatibility`() = runTest { + val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) }) + val presenter = SearchAccountProviderPresenter( + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + changeServerPresenter = { aChangeServerState() } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org")) + val withInputState = awaitItem() + assertThat(withInputState.userInput).isEqualTo("https://test.org") + assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) + assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java) + assertThat(awaitItem().userInputResult).isEqualTo( + AsyncData.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.org") + ) + ) + ) + } + } + @Test fun `present - enter text no result`() = runTest { - val fakeWellknownRetriever = FakeWellknownRetriever() + val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) }) val presenter = SearchAccountProviderPresenter( homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), changeServerPresenter = { aChangeServerState() } @@ -67,48 +89,20 @@ class SearchAccountProviderPresenterTest { } } - @Test - fun `present - enter valid url no wellknown`() = runTest { - val fakeWellknownRetriever = FakeWellknownRetriever() - val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), - changeServerPresenter = { aChangeServerState() } - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org")) - val withInputState = awaitItem() - assertThat(withInputState.userInput).isEqualTo("https://test.org") - assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) - assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java) - assertThat(awaitItem().userInputResult).isEqualTo( - AsyncData.Success( - listOf( - aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false) - ) - ) - ) - } - } - @Test fun `present - enter text one result with wellknown`() = runTest { - val getWellKnownResult = lambdaRecorder> { + val checkResult = lambdaRecorder> { when (it) { - "https://test.org" -> WellknownRetrieverResult.NotFound - "https://test.com" -> WellknownRetrieverResult.NotFound - "https://test.io" -> WellknownRetrieverResult.Success(aWellKnown()) - "https://test" -> WellknownRetrieverResult.NotFound + "https://test.org" -> Result.success(false) + "https://test.com" -> Result.success(false) + "https://test.io" -> Result.success(true) + "https://test" -> Result.success(false) else -> error("should not happen") } } - val fakeWellknownRetriever = FakeWellknownRetriever( - getWellKnownResult = getWellKnownResult, - ) + val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { @@ -127,7 +121,7 @@ class SearchAccountProviderPresenterTest { ) ) ) - getWellKnownResult.assertions().isCalledExactly(4) + checkResult.assertions().isCalledExactly(4) .withSequence( listOf(value("https://test.org")), listOf(value("https://test.com")), @@ -139,20 +133,18 @@ class SearchAccountProviderPresenterTest { @Test fun `present - enter text two results with wellknown`() = runTest { - val getWellKnownResult = lambdaRecorder> { + val checkResult = lambdaRecorder> { when (it) { - "https://test.org" -> WellknownRetrieverResult.Success(aWellKnown()) - "https://test.com" -> WellknownRetrieverResult.NotFound - "https://test.io" -> WellknownRetrieverResult.Success(aWellKnown()) - "https://test" -> WellknownRetrieverResult.NotFound + "https://test.org" -> Result.success(true) + "https://test.com" -> Result.success(false) + "https://test.io" -> Result.success(true) + "https://test" -> Result.success(false) else -> error("should not happen") } } - val fakeWellknownRetriever = FakeWellknownRetriever( - getWellKnownResult = getWellKnownResult, - ) + val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), changeServerPresenter = { aChangeServerState() } ) moleculeFlow(RecompositionMode.Immediate) { @@ -179,7 +171,7 @@ class SearchAccountProviderPresenterTest { ) ) ) - getWellKnownResult.assertions().isCalledExactly(4) + checkResult.assertions().isCalledExactly(4) .withSequence( listOf(value("https://test.org")), listOf(value("https://test.com")), @@ -188,15 +180,4 @@ class SearchAccountProviderPresenterTest { ) } } - - private fun aWellKnown(): WellKnown { - return WellKnown( - homeServer = WellKnownBaseConfig( - baseURL = A_HOMESERVER_URL - ), - identityServer = WellKnownBaseConfig( - baseURL = A_HOMESERVER_URL - ), - ) - } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/HomeServerLoginCompatibilityChecker.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/HomeServerLoginCompatibilityChecker.kt new file mode 100644 index 0000000000..aec1665455 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/HomeServerLoginCompatibilityChecker.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.api.auth + +/** + * Checks the homeserver's compatibility with Element X. + */ +interface HomeServerLoginCompatibilityChecker { + /** + * Performs the compatibility check given the homeserver's [url]. + * @return a `true` value if the homeserver is compatible, `false` if not, or a failure result if the check unexpectedly failed. + */ + suspend fun check(url: String): Result +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt new file mode 100644 index 0000000000..8cfaca077b --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.auth + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import io.element.android.libraries.core.extensions.runCatchingExceptions +import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker +import io.element.android.libraries.matrix.impl.ClientBuilderProvider +import timber.log.Timber + +@ContributesBinding(AppScope::class) +@Inject +class RustHomeServerLoginCompatibilityChecker( + private val clientBuilderProvider: ClientBuilderProvider, +) : HomeServerLoginCompatibilityChecker { + override suspend fun check(url: String): Result = runCatchingExceptions { + clientBuilderProvider.provide() + .inMemoryStore() + .serverNameOrHomeserverUrl(url) + .build() + .use { + it.homeserverLoginDetails() + } + .use { + Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}") + it.supportsOidcLogin() || it.supportsPasswordLogin() + } + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt new file mode 100644 index 0000000000..4557d47f5b --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.auth + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.impl.FakeClientBuilderProvider +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails +import kotlinx.coroutines.test.runTest +import org.junit.Ignore +import org.junit.Test + +@Ignore("JNA direct mapping has broken unit tests with FFI fakes") +class RustHomeserverLoginCompatibilityCheckerTest { + @Test + fun `check - is valid if it supports OIDC login`() = runTest { + val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsOidcLogin = true) } + assertThat(sut.check("https://matrix.host.org").getOrNull()).isTrue() + } + + @Test + fun `check - is valid if it supports password login`() = runTest { + val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsPasswordLogin = true) } + assertThat(sut.check("https://matrix.host.org").getOrNull()).isTrue() + } + + @Test + fun `check - is not valid if it only supports SSO login`() = runTest { + val sut = createChecker { FakeFfiHomeserverLoginDetails(supportsSsoLogin = true) } + assertThat(sut.check("https://matrix.host.org").getOrNull()).isFalse() + } + + @Test + fun `check - is not valid if fetching the data fails`() = runTest { + val sut = createChecker { error("Unexpected error!") } + assertThat(sut.check("https://matrix.host.org").isFailure).isTrue() + } + + private fun createChecker( + result: () -> FakeFfiHomeserverLoginDetails, + ) = RustHomeServerLoginCompatibilityChecker( + clientBuilderProvider = FakeClientBuilderProvider { + FakeFfiClientBuilder { + FakeFfiClient(homeserverLoginDetailsResult = result) + } + } + ) +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt index 85328a2d4d..7951309bcd 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt @@ -12,10 +12,12 @@ import org.matrix.rustcomponents.sdk.NoHandle class FakeFfiHomeserverLoginDetails( private val url: String = "https://example.org", - private val supportsPasswordLogin: Boolean = true, - private val supportsOidcLogin: Boolean = false + private val supportsPasswordLogin: Boolean = false, + private val supportsOidcLogin: Boolean = false, + private val supportsSsoLogin: Boolean = false, ) : HomeserverLoginDetails(NoHandle) { override fun url(): String = url override fun supportsOidcLogin(): Boolean = supportsOidcLogin override fun supportsPasswordLogin(): Boolean = supportsPasswordLogin + override fun supportsSsoLogin(): Boolean = supportsSsoLogin } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeHomeServerLoginCompatibilityChecker.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeHomeServerLoginCompatibilityChecker.kt new file mode 100644 index 0000000000..934784b684 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeHomeServerLoginCompatibilityChecker.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.test.auth + +import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker + +class FakeHomeServerLoginCompatibilityChecker( + private val checkResult: (String) -> Result, +) : HomeServerLoginCompatibilityChecker { + override suspend fun check(url: String): Result { + return checkResult(url) + } +} diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt index 1c5570db94..2a986fcc28 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/SessionWellknownRetriever.kt @@ -8,6 +8,5 @@ package io.element.android.libraries.wellknown.api interface SessionWellknownRetriever { - suspend fun getWellKnown(): WellknownRetrieverResult suspend fun getElementWellKnown(): WellknownRetrieverResult } diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt deleted file mode 100644 index 59f63d1655..0000000000 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellKnown.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.wellknown.api - -data class WellKnown( - val homeServer: WellKnownBaseConfig?, - val identityServer: WellKnownBaseConfig?, -) - -data class WellKnownBaseConfig( - val baseURL: String? -) diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt index 4675a0cb18..5c146fbbad 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt @@ -8,6 +8,5 @@ package io.element.android.libraries.wellknown.api interface WellknownRetriever { - suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt index ad463bd2d2..a4b394a427 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetriever.kt @@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.SessionWellknownRetriever -import io.element.android.libraries.wellknown.api.WellKnown import io.element.android.libraries.wellknown.api.WellknownRetrieverResult import timber.log.Timber @@ -26,17 +25,6 @@ class DefaultSessionWellknownRetriever( ) : SessionWellknownRetriever { private val domain by lazy { matrixClient.userIdServerName() } - override suspend fun getWellKnown(): WellknownRetrieverResult { - val url = "https://$domain/.well-known/matrix/client" - return matrixClient - .getUrl(url) - .mapCatchingExceptions { - val data = String(it) - json().decodeFromString(data).map() - } - .toWellknownRetrieverResult() - } - override suspend fun getElementWellKnown(): WellknownRetrieverResult { val url = "https://$domain/.well-known/element/element.json" return matrixClient diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt index a6014f773c..1312436654 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt @@ -13,7 +13,6 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.wellknown.api.ElementWellKnown -import io.element.android.libraries.wellknown.api.WellKnown import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetrieverResult import retrofit2.HttpException @@ -24,27 +23,6 @@ import java.net.HttpURLConnection class DefaultWellknownRetriever( private val retrofitFactory: RetrofitFactory, ) : WellknownRetriever { - override suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult { - return buildWellknownApi(baseUrl) - .map { wellknownApi -> - try { - val result = wellknownApi.getWellKnown().map() - WellknownRetrieverResult.Success(result) - } catch (e: Exception) { - Timber.e(e, "Failed to retrieve well-known data for $baseUrl") - if ((e as? HttpException)?.code() == HttpURLConnection.HTTP_NOT_FOUND) { - WellknownRetrieverResult.NotFound - } else { - WellknownRetrieverResult.Error(e) - } - } - } - .fold( - onSuccess = { it }, - onFailure = { WellknownRetrieverResult.Error(it as Exception) } - ) - } - override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult { return buildWellknownApi(baseUrl) .map { wellknownApi -> diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt index 169757caa9..3b705e09c8 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt @@ -8,8 +8,6 @@ package io.element.android.libraries.wellknown.impl import io.element.android.libraries.wellknown.api.ElementWellKnown -import io.element.android.libraries.wellknown.api.WellKnown -import io.element.android.libraries.wellknown.api.WellKnownBaseConfig internal fun InternalElementWellKnown.map() = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, @@ -17,12 +15,3 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown( rageshakeUrl = rageshakeUrl, brandColor = brandColor, ) - -internal fun InternalWellKnown.map() = WellKnown( - homeServer = homeServer?.map(), - identityServer = identityServer?.map(), -) - -internal fun InternalWellKnownBaseConfig.map() = WellKnownBaseConfig( - baseURL = baseURL, -) diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt index 12b961ff9e..63dca38aac 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -12,8 +12,6 @@ import io.element.android.libraries.androidutils.json.DefaultJsonProvider import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.wellknown.api.ElementWellKnown -import io.element.android.libraries.wellknown.api.WellKnown -import io.element.android.libraries.wellknown.api.WellKnownBaseConfig import io.element.android.libraries.wellknown.api.WellknownRetrieverResult import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -21,142 +19,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Test class DefaultSessionWellknownRetrieverTest { - @Test - fun `get empty wellknown`() = runTest { - val getUrlLambda = lambdaRecorder> { - Result.success("{}".toByteArray()) - } - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = getUrlLambda, - ) - assertThat(sut.getWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - WellKnown( - homeServer = null, - identityServer = null, - ) - ) - ) - getUrlLambda.assertions().isCalledOnce() - .with(value("https://user.domain.org/.well-known/matrix/client")) - } - - @Test - fun `get wellknown with full content`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "m.homeserver": { - "base_url": "https://example.org" - }, - "m.identity_server": { - "base_url": "https://identity.example.org" - } - }""".trimIndent().toByteArray() - ) - } - ) - assertThat(sut.getWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - WellKnown( - homeServer = WellKnownBaseConfig( - baseURL = "https://example.org", - ), - identityServer = WellKnownBaseConfig( - baseURL = "https://identity.example.org", - ), - ) - ) - ) - } - - @Test - fun `get wellknown with full content empty base_url`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "m.homeserver": { - "base_url": "https://example.org" - }, - "m.identity_server": {} - }""".trimIndent().toByteArray() - ) - } - ) - assertThat(sut.getWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - WellKnown( - homeServer = WellKnownBaseConfig( - baseURL = "https://example.org", - ), - identityServer = WellKnownBaseConfig( - baseURL = null, - ), - ) - ) - ) - } - - @Test - fun `get wellknown with unknown key`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "m.homeserver": { - "base_url": "https://example.org" - }, - "m.identity_server": { - "base_url": "https://identity.example.org" - }, - "other": true - }""".trimIndent().toByteArray() - ) - }, - ) - assertThat(sut.getWellKnown()).isEqualTo( - WellknownRetrieverResult.Success( - WellKnown( - homeServer = WellKnownBaseConfig( - baseURL = "https://example.org", - ), - identityServer = WellKnownBaseConfig( - baseURL = "https://identity.example.org", - ), - ) - ) - ) - } - - @Test - fun `get wellknown json error`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.success( - """{ - "m.homeserver": { - "base_url": "https://example.org" - }, - error - }""".trimIndent().toByteArray() - ) - } - ) - assertThat(sut.getWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) - } - - @Test - fun `get wellknown network error`() = runTest { - val sut = createDefaultSessionWellknownRetriever( - getUrlLambda = { - Result.failure(AN_EXCEPTION) - } - ) - assertThat(sut.getWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java) - } - @Test fun `get empty element wellknown`() = runTest { val getUrlLambda = lambdaRecorder> { diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeSessionWellknownRetriever.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeSessionWellknownRetriever.kt index d90a02ac96..5ab66701be 100644 --- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeSessionWellknownRetriever.kt +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeSessionWellknownRetriever.kt @@ -9,18 +9,12 @@ package io.element.android.features.wellknown.test import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.SessionWellknownRetriever -import io.element.android.libraries.wellknown.api.WellKnown import io.element.android.libraries.wellknown.api.WellknownRetrieverResult import io.element.android.tests.testutils.simulateLongTask class FakeSessionWellknownRetriever( - private val getWellKnownResult: () -> WellknownRetrieverResult = { WellknownRetrieverResult.NotFound }, private val getElementWellKnownResult: () -> WellknownRetrieverResult = { WellknownRetrieverResult.NotFound }, ) : SessionWellknownRetriever { - override suspend fun getWellKnown(): WellknownRetrieverResult = simulateLongTask { - getWellKnownResult() - } - override suspend fun getElementWellKnown(): WellknownRetrieverResult = simulateLongTask { getElementWellKnownResult() } diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt index 52bb1cfa4c..4c4a31a572 100644 --- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt @@ -8,19 +8,13 @@ package io.element.android.features.wellknown.test import io.element.android.libraries.wellknown.api.ElementWellKnown -import io.element.android.libraries.wellknown.api.WellKnown import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetrieverResult import io.element.android.tests.testutils.simulateLongTask class FakeWellknownRetriever( - private val getWellKnownResult: (String) -> WellknownRetrieverResult = { WellknownRetrieverResult.NotFound }, private val getElementWellKnownResult: (String) -> WellknownRetrieverResult = { WellknownRetrieverResult.NotFound }, ) : WellknownRetriever { - override suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult = simulateLongTask { - getWellKnownResult(baseUrl) - } - override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult = simulateLongTask { getElementWellKnownResult(baseUrl) } From fb4114adad513789ffc74c01483b6eb65c3ff365 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 Nov 2025 15:43:23 +0100 Subject: [PATCH 48/58] Revert "NotificationDataFactory: improve API" This reverts commit b0e5e6cc616a1561aa4e256821f67cc206ada6df. # Conflicts: # libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDataFactoryTest.kt --- .../notifications/NotificationDataFactory.kt | 32 ++++++++++++------- .../notifications/NotificationRenderer.kt | 16 +++------- .../NotificationDataFactoryTest.kt | 15 ++++++--- .../fake/FakeNotificationDataFactory.kt | 20 +++++++----- 4 files changed, 46 insertions(+), 37 deletions(-) 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 ab8406f1af..33b2df410c 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 @@ -28,26 +28,30 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import io.element.android.services.toolbox.api.strings.StringProvider interface NotificationDataFactory { - suspend fun List.toNotifications( + suspend fun toNotifications( + messages: List, imageLoader: ImageLoader, notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationInvites") @Suppress("INAPPLICABLE_JVM_NAME") - fun List.toNotifications( + fun toNotifications( + invites: List, notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationSimpleEvents") @Suppress("INAPPLICABLE_JVM_NAME") - fun List.toNotifications( + fun toNotifications( + simpleEvents: List, notificationAccountParams: NotificationAccountParams, ): List @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - fun List.toNotifications( + fun toNotifications( + fallback: List, notificationAccountParams: NotificationAccountParams, ): List @@ -68,11 +72,12 @@ class DefaultNotificationDataFactory( private val activeNotificationsProvider: ActiveNotificationsProvider, private val stringProvider: StringProvider, ) : NotificationDataFactory { - override suspend fun List.toNotifications( + override suspend fun toNotifications( + messages: List, imageLoader: ImageLoader, notificationAccountParams: NotificationAccountParams, ): List { - val messagesToDisplay = filterNot { it.canNotBeDisplayed() } + val messagesToDisplay = messages.filterNot { it.canNotBeDisplayed() } .groupBy { it.roomId } return messagesToDisplay.flatMap { (roomId, events) -> val roomName = events.lastOrNull()?.roomName ?: roomId.value @@ -109,10 +114,11 @@ class DefaultNotificationDataFactory( @JvmName("toNotificationInvites") @Suppress("INAPPLICABLE_JVM_NAME") - override fun List.toNotifications( + override fun toNotifications( + invites: List, notificationAccountParams: NotificationAccountParams, ): List { - return map { event -> + return invites.map { event -> OneShotNotification( tag = event.roomId.value, notification = notificationCreator.createRoomInvitationNotification(notificationAccountParams, event), @@ -125,10 +131,11 @@ class DefaultNotificationDataFactory( @JvmName("toNotificationSimpleEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun List.toNotifications( + override fun toNotifications( + simpleEvents: List, notificationAccountParams: NotificationAccountParams, ): List { - return map { event -> + return simpleEvents.map { event -> OneShotNotification( tag = event.eventId.value, notification = notificationCreator.createSimpleEventNotification(notificationAccountParams, event), @@ -141,10 +148,11 @@ class DefaultNotificationDataFactory( @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun List.toNotifications( + override fun toNotifications( + fallback: List, notificationAccountParams: NotificationAccountParams, ): List { - return map { event -> + return fallback.map { event -> OneShotNotification( tag = event.eventId.value, notification = notificationCreator.createFallbackNotification(notificationAccountParams, event), 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 9f24ccecb0..0cfe083caf 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 @@ -51,18 +51,10 @@ class NotificationRenderer( showSessionId = numberOfAccounts > 1, ) val groupedEvents = eventsToProcess.groupByType() - val roomNotifications = with(notificationDataFactory) { - groupedEvents.roomEvents.toNotifications(imageLoader, notificationAccountParams) - } - val invitationNotifications = with(notificationDataFactory) { - groupedEvents.invitationEvents.toNotifications(notificationAccountParams) - } - val simpleNotifications = with(notificationDataFactory) { - groupedEvents.simpleEvents.toNotifications(notificationAccountParams) - } - val fallbackNotifications = with(notificationDataFactory) { - groupedEvents.fallbackEvents.toNotifications(notificationAccountParams) - } + 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 summaryNotification = notificationDataFactory.createSummaryNotification( roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, 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 50e0b5dfb8..6971fcbc41 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 @@ -55,7 +55,9 @@ class NotificationDataFactoryTest { aNotificationAccountParams(), AN_INVITATION_EVENT, ) - val result = listOf(AN_INVITATION_EVENT).toNotifications(aNotificationAccountParams()) + val roomInvitation = listOf(AN_INVITATION_EVENT) + val result = toNotifications(roomInvitation, aNotificationAccountParams()) + assertThat(result).isEqualTo( listOf( OneShotNotification( @@ -75,7 +77,7 @@ class NotificationDataFactoryTest { aNotificationAccountParams(), AN_INVITATION_EVENT, ) - val result = listOf(A_SIMPLE_EVENT).toNotifications(aNotificationAccountParams()) + val result = toNotifications(listOf(A_SIMPLE_EVENT), aNotificationAccountParams()) assertThat(result).containsExactly( OneShotNotification( notification = expectedNotification, @@ -109,7 +111,8 @@ class NotificationDataFactoryTest { threadId = null, ) val fakeImageLoader = FakeImageLoader() - val result = listOf(A_MESSAGE_EVENT).toNotifications( + val result = toNotifications( + messages = listOf(A_MESSAGE_EVENT), notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), @@ -125,7 +128,8 @@ class NotificationDataFactoryTest { fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationDataFactory) { val redactedRoom = A_MESSAGE_EVENT.copy(isRedacted = true) val fakeImageLoader = FakeImageLoader() - val result = listOf(redactedRoom).toNotifications( + val result = toNotifications( + messages = listOf(redactedRoom), notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), @@ -164,7 +168,8 @@ class NotificationDataFactoryTest { ) val fakeImageLoader = FakeImageLoader() - val result = roomWithRedactedMessage.toNotifications( + val result = toNotifications( + messages = roomWithRedactedMessage, notificationAccountParams = aNotificationAccountParams( user = MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), ), 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 89326f5ad9..a897dbfc09 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 @@ -40,35 +40,39 @@ class FakeNotificationDataFactory( var fallbackEventToNotificationsResult: LambdaOneParamRecorder, List> = lambdaRecorder { _ -> emptyList() }, ) : NotificationDataFactory { - override suspend fun List.toNotifications( + override suspend fun toNotifications( + messages: List, imageLoader: ImageLoader, notificationAccountParams: NotificationAccountParams, ): List { - return messageEventToNotificationsResult(this, imageLoader, notificationAccountParams) + return messageEventToNotificationsResult(messages, imageLoader, notificationAccountParams) } @JvmName("toNotificationInvites") @Suppress("INAPPLICABLE_JVM_NAME") - override fun List.toNotifications( + override fun toNotifications( + invites: List, notificationAccountParams: NotificationAccountParams, ): List { - return inviteToNotificationsResult(this) + return inviteToNotificationsResult(invites) } @JvmName("toNotificationSimpleEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun List.toNotifications( + override fun toNotifications( + simpleEvents: List, notificationAccountParams: NotificationAccountParams, ): List { - return simpleEventToNotificationsResult(this) + return simpleEventToNotificationsResult(simpleEvents) } @JvmName("toNotificationFallbackEvents") @Suppress("INAPPLICABLE_JVM_NAME") - override fun List.toNotifications( + override fun toNotifications( + fallback: List, notificationAccountParams: NotificationAccountParams, ): List { - return fallbackEventToNotificationsResult(this) + return fallbackEventToNotificationsResult(fallback) } override fun createSummaryNotification( From 16fcc12e93e84915bf0eb85e7e7bbee847f957ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 Nov 2025 15:45:08 +0100 Subject: [PATCH 49/58] Remove unused property --- .../impl/notifications/DefaultNotificationDrawerManager.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 1677072399..6d52cbfe13 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl.notifications import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn -import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId @@ -30,8 +29,6 @@ import io.element.android.services.appnavstate.api.currentSessionId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.NotificationLoggerTag) - /** * This class receives notification events as they arrive from the PushHandler calling [onNotifiableEventReceived] and * organise them in order to display them in the notification drawer. From dd328c5a0801c79234cefb9b81e35f3dbfb604cb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 Nov 2025 16:22:16 +0100 Subject: [PATCH 50/58] Use new SessionStore API --- .../libraries/push/impl/notifications/NotificationRenderer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0cfe083caf..5da7389125 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 @@ -44,7 +44,7 @@ class NotificationRenderer( ) { val color = enterpriseService.brandColorsFlow(currentUser.userId).first()?.toArgb() ?: NotificationConfig.NOTIFICATION_ACCENT_COLOR - val numberOfAccounts = sessionStore.getAllSessions().size + val numberOfAccounts = sessionStore.numberOfSessions() val notificationAccountParams = NotificationAccountParams( user = currentUser, color = color, From cf011795bf8ec497ffdb6462cecfec3a9a932ac5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:25:56 +0000 Subject: [PATCH 51/58] fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.11.4 (#5681) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d17795bcca..e8a0b2bea8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -177,7 +177,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # All new features should not be implemented in the pull request that upgrades the version, developers should # only fix API breaks and may add some TODOs. -matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.10.31" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.11.4" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } From 09aaab89d004a3fffb9665e6b71a42c993c3ba06 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 02:42:27 +0000 Subject: [PATCH 52/58] fix(deps): update metro to v0.7.4 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e8a0b2bea8..5cfc639d67 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ haze = "1.6.10" dependencyAnalysis = "3.4.1" # DI -metro = "0.7.3" +metro = "0.7.4" # Auto service autoservice = "1.1.1" From f9f056bf85989e15d5164231183010be3a5c8168 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Nov 2025 11:00:13 +0100 Subject: [PATCH 53/58] Do not use the bastDescription but the cation for image/video/sticker because else the filename will be rendered in the notification and for media we do not want that. Also fixes the issue when images is not rendered on some system and so they can be empty notification. Closes #3945 --- .../DefaultNotifiableEventResolver.kt | 58 +++++++++++++++---- .../factories/NotificationCreator.kt | 50 +++++++++------- 2 files changed, 76 insertions(+), 32 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index 481d354e8d..5e397c94d9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -8,7 +8,10 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context +import android.graphics.ImageDecoder import android.net.Uri +import android.os.Build +import androidx.core.app.NotificationCompat import androidx.core.content.FileProvider import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding @@ -138,7 +141,13 @@ class DefaultNotifiableEventResolver( is NotificationContent.MessageLike.RoomMessage -> { val showMediaPreview = client.mediaPreviewService.getMediaPreviewValue() == MediaPreviewValue.On val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId) - val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName) + val imageMimeType = if (showMediaPreview) content.getImageMimetype() else null + val imageUriString = imageMimeType?.let { content.fetchImageIfPresent(client, imageMimeType)?.toString() } + val messageBody = descriptionFromMessageContent( + content = content, + senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, + hasImageUri = imageUriString != null, + ) val notifiableMessageEvent = buildNotifiableMessageEvent( sessionId = userId, senderId = content.senderId, @@ -149,8 +158,8 @@ class DefaultNotifiableEventResolver( timestamp = this.timestamp, senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, body = messageBody, - imageUriString = if (showMediaPreview) content.fetchImageIfPresent(client)?.toString() else null, - imageMimeType = if (showMediaPreview) content.getImageMimetype() else null, + imageUriString = imageUriString, + imageMimeType = imageMimeType.takeIf { imageUriString != null }, roomName = roomDisplayName, roomIsDm = isDm, roomAvatarPath = roomAvatarUrl, @@ -299,13 +308,18 @@ class DefaultNotifiableEventResolver( private fun descriptionFromMessageContent( content: NotificationContent.MessageLike.RoomMessage, senderDisambiguatedDisplayName: String, - ): String { + hasImageUri: Boolean, + ): String? { return when (val messageType = content.messageType) { is AudioMessageType -> messageType.bestDescription is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message) is EmoteMessageType -> "* $senderDisambiguatedDisplayName ${messageType.body}" is FileMessageType -> messageType.bestDescription - is ImageMessageType -> messageType.bestDescription + is ImageMessageType -> if (hasImageUri) { + messageType.caption + } else { + messageType.bestDescription + } is StickerMessageType -> messageType.bestDescription is NoticeMessageType -> messageType.body is TextMessageType -> messageType.toPlainText(permalinkParser = permalinkParser) @@ -326,14 +340,34 @@ class DefaultNotifiableEventResolver( } } - private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent(client: MatrixClient): Uri? { + /** + * Fetch the image for message type, only if the mime type is supported, as recommended + * per [NotificationCompat.MessagingStyle.Message.setData] documentation. + * Then convert to a [Uri] accessible to the Notification Service. + */ + private suspend fun NotificationContent.MessageLike.RoomMessage.fetchImageIfPresent( + client: MatrixClient, + mimeType: String, + ): Uri? { val fileResult = when (val messageType = messageType) { - is ImageMessageType -> notificationMediaRepoFactory.create(client) - .getMediaFile( - mediaSource = messageType.source, - mimeType = messageType.info?.mimetype, - filename = messageType.filename, - ) + is ImageMessageType -> { + val isMimeTypeSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ImageDecoder.isMimeTypeSupported(mimeType) + } else { + // Assume it's supported on old systems... + true + } + if (isMimeTypeSupported) { + notificationMediaRepoFactory.create(client).getMediaFile( + mediaSource = messageType.source, + mimeType = messageType.info?.mimetype, + filename = messageType.filename, + ) + } else { + Timber.tag(loggerTag.value).d("Mime type $mimeType not supported by the system") + null + } + } is VideoMessageType -> null // Use the thumbnail here? else -> null } 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 efe54bd3d9..0dccdb46c2 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 @@ -431,27 +431,37 @@ class DefaultNotificationCreator( senderPerson ) else -> { - val message = MessagingStyle.Message( - event.body?.annotateForDebug(71), - event.timestamp, - senderPerson - ).also { message -> - event.imageUri?.let { - message.setData(event.imageMimeType ?: "image/", it) - } - message.extras.putString(MESSAGE_EVENT_ID, event.eventId.value) - } - addMessage(message) - - // Add additional message for captions - if (event.imageUri != null && event.body != null) { - addMessage( - MessagingStyle.Message( - event.body, - event.timestamp, - senderPerson, - ) + if (event.imageMimeType != null && event.imageUri != null) { + // Image case + val message = MessagingStyle.Message( + // This text will not be rendered, but some systems does not render the image + // if the text is null + stringProvider.getString(CommonStrings.common_image), + event.timestamp, + senderPerson, ) + .setData(event.imageMimeType, event.imageUri) + message.extras.putString(MESSAGE_EVENT_ID, event.eventId.value) + addMessage(message) + // Add additional message for captions + if (event.body != null) { + addMessage( + MessagingStyle.Message( + event.body.annotateForDebug(72), + event.timestamp, + senderPerson, + ) + ) + } + } else { + // Text case + val message = MessagingStyle.Message( + event.body?.annotateForDebug(71), + event.timestamp, + senderPerson + ) + message.extras.putString(MESSAGE_EVENT_ID, event.eventId.value) + addMessage(message) } } } From baa6dbd0c45648402414e3f62605f1d8a1ed1db4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Nov 2025 11:47:51 +0100 Subject: [PATCH 54/58] Fix test. --- .../DefaultNotificationDrawerManagerTest.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 ff484df96f..0489b72dd3 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 @@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.Color import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService -import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -118,8 +117,11 @@ class DefaultNotificationDrawerManagerTest { } @Test - fun `when MatrixClient has no cached user name a fallback one is used to render the notification`() = runTest { - val matrixClient = FakeMatrixClient(userDisplayName = null) + fun `when MatrixClient has no cached user name and avatar, the profile is loaded to render the notification`() = runTest { + val matrixClient = FakeMatrixClient( + userDisplayName = null, + userAvatarUrl = null, + ) val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) val messageCreator = FakeRoomGroupMessageCreator() val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( @@ -153,7 +155,7 @@ class DefaultNotificationDrawerManagerTest { any(), ), listOf( - value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value))), + value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = ""))), any(), any(), any(), @@ -161,7 +163,7 @@ class DefaultNotificationDrawerManagerTest { any(), ), listOf( - value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = A_SESSION_ID.value, avatarUrl = AN_AVATAR_URL))), + value(aNotificationAccountParams(user = aMatrixUser(id = A_SESSION_ID.value, displayName = null, avatarUrl = null))), any(), any(), any(), From 36a2ecff6676b23e6f095c6152e3db4c1e865d4a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Nov 2025 11:53:18 +0100 Subject: [PATCH 55/58] Setting version for the release 25.11.2 --- plugins/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 819554ae55..d82e203e9b 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -44,7 +44,7 @@ private const val versionMonth = 11 * Release number in the month. Value must be in [0,99]. * Do not update this value. it is updated by the release script. */ -private const val versionReleaseNumber = 1 +private const val versionReleaseNumber = 2 object Versions { /** From fb79421f4d8ebd3646bed8bb0b244def1941f23a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Nov 2025 11:53:37 +0100 Subject: [PATCH 56/58] Adding fastlane file for version 25.11.2 --- fastlane/metadata/android/en-US/changelogs/202511020.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/202511020.txt diff --git a/fastlane/metadata/android/en-US/changelogs/202511020.txt b/fastlane/metadata/android/en-US/changelogs/202511020.txt new file mode 100644 index 0000000000..a4b397f1bb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202511020.txt @@ -0,0 +1,2 @@ +Main changes in this version: bug fixes and improvements. +Full changelog: https://github.com/element-hq/element-x-android/releases \ No newline at end of file From cdd6744135a87210ba48975fec9cd2febd408f2a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 5 Nov 2025 12:48:47 +0100 Subject: [PATCH 57/58] Changelog for version 25.11.2 --- CHANGES.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 1ac0cd1124..28e774a8ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,86 @@ +Changes in Element X v25.11.2 +============================= + + + +## What's Changed +### ✨ Features +* Enable access to security and privacy by @bmarty in https://github.com/element-hq/element-x-android/pull/5566 +* Add ability to forward a media from the media viewer and the gallery by @bmarty in https://github.com/element-hq/element-x-android/pull/5622 +* Split notifications for messages in threads by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5595 +### 🙌 Improvements +* Enable `SyncNotificationsWithWorkManager` in nightly and debug builds by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5573 +* Confirm exit without saving change in room details edit screen by @bmarty in https://github.com/element-hq/element-x-android/pull/5618 +* Space : add view members entry by @ganfra in https://github.com/element-hq/element-x-android/pull/5619 +* Update notification sound by @bmarty in https://github.com/element-hq/element-x-android/pull/5667 +* Use the new notification sound only on debug and nightly build by @bmarty in https://github.com/element-hq/element-x-android/pull/5673 +* Make sure we know the session verification state before showing the options to verify the session by @bmarty in https://github.com/element-hq/element-x-android/pull/5677 +### 🐛 Bugfixes +* Improve how brand color is applied. by @bmarty in https://github.com/element-hq/element-x-android/pull/5584 +* Improve wellknown retrieval API by @bmarty in https://github.com/element-hq/element-x-android/pull/5587 +* Clearing the room list search clears the search term too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5603 +* Delete pin code only when the last session is deleted by @bmarty in https://github.com/element-hq/element-x-android/pull/5600 +* Fix issues with WorkManager on Android 12 and below by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5606 +* Fix marking a room as read re-instantiates its timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5628 +* Display only valid emojis in recent emoji list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5612 +* Fix navigation issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/5666 +* Fix forward events from media viewer from pinned media timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/5669 +* Try fixing 'Timeline Event object has already been destroyed' by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5675 +* Use the SDK Client to check whether a homeserver is compatible by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5664 +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5610 +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/5662 +### 🧱 Build +* Remove `@Inject`, not necessary anymore when class is annotated with `@ContributesBinding` by @bmarty in https://github.com/element-hq/element-x-android/pull/5589 +* Upgrade ktlint to 1.7.1 and ensure Renovate will upgrade the version by @bmarty in https://github.com/element-hq/element-x-android/pull/5638 +* Improve architecture around Nodes by @bmarty in https://github.com/element-hq/element-x-android/pull/5641 +* Move dependencies block out of the android block. by @bmarty in https://github.com/element-hq/element-x-android/pull/5674 +* Always use the handleEvent(s) function the same way. by @bmarty in https://github.com/element-hq/element-x-android/pull/5672 +### Dependency upgrades +* fix(deps): update metro to v0.7.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5576 +* fix(deps): update dependencyanalysis to v3.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5577 +* fix(deps): update dependency io.sentry:sentry-android to v8.24.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5586 +* fix(deps): update dependency androidx.work:work-runtime-ktx to v2.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5590 +* fix(deps): update dependency com.posthog:posthog-android to v3.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5594 +* fix(deps): update dependency com.google.crypto.tink:tink-android to v1.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5572 +* Update plugin sonarqube to v7.0.1.6134 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5605 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.28 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5620 +* fix(deps): update dependencyanalysis to v3.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5602 +* fix(deps): update dependency com.github.matrix-org:matrix-analytics-events to v0.29.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5621 +* fix(deps): update dependencyanalysis to v3.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5624 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.29 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5625 +* fix(deps): update dependency io.sentry:sentry-android to v8.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5629 +* fix(deps): update dependencyanalysis to v3.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5642 +* fix(deps): update dependency com.squareup.okhttp3:okhttp-bom to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5644 +* chore(deps): update danger/danger-js action to v13.0.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5652 +* fix(deps): update dependency com.google.firebase:firebase-bom to v34.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5643 +* fix(deps): update firebaseappdistribution to v5.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5640 +* fix(deps): update metro to v0.7.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5663 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.10.31 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5657 +* Update GitHub Artifact Actions (major) by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5609 +* Update dependency io.element.android:element-call-embedded to v0.16.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5598 +* Update roborazzi to v1.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5676 +* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25.11.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5681 +* fix(deps): update metro to v0.7.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/5683 +### Others +* Improve code around Element .well-known configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/5565 +* misc: display offline banner for all LoggedIn screens by @ganfra in https://github.com/element-hq/element-x-android/pull/5574 +* Remove icon preview duplicate by @bmarty in https://github.com/element-hq/element-x-android/pull/5588 +* Remove application navigation state usage in the push module by @bmarty in https://github.com/element-hq/element-x-android/pull/5596 +* Design : update Home TopBar and RoomList Filters by @ganfra in https://github.com/element-hq/element-x-android/pull/5599 +* Add missing tests on the analytic modules by @bmarty in https://github.com/element-hq/element-x-android/pull/5604 +* design(space): let SpaceRoomItemView divider be full width by @ganfra in https://github.com/element-hq/element-x-android/pull/5597 +* Update notification style by @bmarty in https://github.com/element-hq/element-x-android/pull/5607 +* Improve how data is handled for the WorkManager. by @bmarty in https://github.com/element-hq/element-x-android/pull/5592 +* Revert "Make sure declining a call stops observing the ringing call state" by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5615 +* Misc : space flow inject room by @ganfra in https://github.com/element-hq/element-x-android/pull/5614 +* Enable `SyncNotificationsWithWorkManager` by default in release mode apps too by @jmartinesp in https://github.com/element-hq/element-x-android/pull/5646 +* Revert "Update notification sound" by @bmarty in https://github.com/element-hq/element-x-android/pull/5671 +* Introduce new query to count accounts by @bmarty in https://github.com/element-hq/element-x-android/pull/5678 + + +**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.11.0...v25.11.2 + Changes in Element X v25.11.0 ============================= From f4ef9a0545b2f113e3b7f8d5dc277ae3ab084734 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 5 Nov 2025 15:42:59 +0100 Subject: [PATCH 58/58] quality: use callback() method in nodes --- .../android/features/space/impl/leave/LeaveSpaceNode.kt | 4 ++-- .../android/features/space/impl/settings/SpaceSettingsNode.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt index 6ba86481fe..6eaa5d4e4a 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceNode.kt @@ -13,11 +13,11 @@ import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.core.plugin.plugins import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.space.impl.di.SpaceFlowScope +import io.element.android.libraries.architecture.callback import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.room.JoinedRoom @@ -38,7 +38,7 @@ class LeaveSpaceNode( private val leaveSpaceHandle = matrixClient.spaceService.getLeaveSpaceHandle(room.roomId) private val presenter: LeaveSpacePresenter = presenterFactory.create(leaveSpaceHandle) - private val callback = plugins().single() + private val callback: Callback = callback() override fun onBuilt() { super.onBuilt() diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt index 9b81272153..ea2e07db77 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsNode.kt @@ -14,12 +14,12 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.core.plugin.plugins import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.space.impl.di.SpaceFlowScope import io.element.android.libraries.architecture.appyx.launchMolecule +import io.element.android.libraries.architecture.callback @ContributesNode(SpaceFlowScope::class) @AssistedInject @@ -38,7 +38,7 @@ class SpaceSettingsNode( fun startLeaveSpaceFlow() } - private val callback = plugins().single() + private val callback: Callback = callback() private val stateFlow = launchMolecule { presenter.present() } @Composable