From e047bfd75fdc5507f55aeaba39bc1c8190fc9ea5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 May 2024 11:47:38 +0200 Subject: [PATCH 01/12] CI : fix release workflow - concurrency group --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3998bc0246..8feecd0db9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: name: Create App Bundle (Gplay) runs-on: ubuntu-latest concurrency: - group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }} + group: ${{ github.ref == 'refs/head/main' && format('build-release-main-gplay-{0}', github.sha) }} cancel-in-progress: true steps: - uses: actions/checkout@v4 @@ -43,7 +43,7 @@ jobs: name: Create APKs (FDroid) runs-on: ubuntu-latest concurrency: - group: ${{ github.ref == 'refs/head/main' && format('build-release-main-{0}', github.sha) }} + group: ${{ github.ref == 'refs/head/main' && format('build-release-main-fdroid-{0}', github.sha) }} cancel-in-progress: true steps: - uses: actions/checkout@v4 From 2f29a0040de65043ee4d6ec0584e4003397f533e Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 7 Jun 2024 13:46:42 +0200 Subject: [PATCH 02/12] CI : fix the release script for fdroid --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3cb7b27c3a..777381f589 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: name: Create App Bundle (Gplay) runs-on: ubuntu-latest concurrency: - group: ${{ github.ref == 'refs/head/main' && format('build-release-main-gplay-{0}', github.sha) }} + group: ${{ format('build-release-main-gplay-{0}', github.sha) }} cancel-in-progress: true steps: - uses: actions/checkout@v4 @@ -43,7 +43,7 @@ jobs: name: Create APKs (FDroid) runs-on: ubuntu-latest concurrency: - group: ${{ github.ref == 'refs/head/main' && format('build-release-main-fdroid-{0}', github.sha) }} + group: ${{ format('build-release-main-fdroid-{0}', github.sha) }} cancel-in-progress: true steps: - uses: actions/checkout@v4 From 01897accfc1e83fc78b6e18f6b14e44c38330647 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 28 Aug 2024 16:47:56 +0200 Subject: [PATCH 03/12] Fix maplibre annotation plugins dependency --- gradle/libs.versions.toml | 1 + libraries/maplibre-compose/build.gradle.kts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81f93b1c0e..d6b552feff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -180,6 +180,7 @@ statemachine = "com.freeletics.flowredux:compose:1.2.2" maplibre = "org.maplibre.gl:android-sdk:11.2.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.0" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.0" +mapbox_android_gestures = "com.mapbox.mapboxsdk:mapbox-android-gestures:0.7.0" opusencoder = "io.element.android:opusencoder:1.1.0" kotlinpoet = "com.squareup:kotlinpoet:1.18.1" zxing_cpp = "io.github.zxing-cpp:android:2.2.0" diff --git a/libraries/maplibre-compose/build.gradle.kts b/libraries/maplibre-compose/build.gradle.kts index e2a9b821ba..b542f853b6 100644 --- a/libraries/maplibre-compose/build.gradle.kts +++ b/libraries/maplibre-compose/build.gradle.kts @@ -31,4 +31,6 @@ dependencies { api(libs.maplibre) api(libs.maplibre.ktx) api(libs.maplibre.annotation) + // needed for libs.maplibre.annotation waiting for a new release with the fix + implementation(libs.mapbox.android.gestures) } From a556557881c1f631747f2a50780017c58e33adf5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 9 Sep 2024 11:20:50 +0200 Subject: [PATCH 04/12] Timeline : remove the encrypted history banner for now. --- .../components/TimelineItemVirtualRow.kt | 3 - .../TimelineEncryptedHistoryBannerView.kt | 61 ---------- .../virtual/TimelineItemVirtualFactory.kt | 2 - ...eItemEncryptedHistoryBannerVirtualModel.kt | 12 -- .../item/virtual/VirtualTimelineItem.kt | 2 - .../libraries/matrix/impl/RustMatrixClient.kt | 2 - .../matrix/impl/room/RustMatrixRoom.kt | 7 +- .../matrix/impl/room/RustRoomFactory.kt | 2 - .../matrix/impl/timeline/RustTimeline.kt | 11 -- .../HasEncryptionHistoryBanner.kt | 17 --- .../LoadingIndicatorsPostProcessor.kt | 3 +- .../RoomBeginningPostProcessor.kt | 1 - .../TimelineEncryptedHistoryPostProcessor.kt | 55 --------- .../RoomBeginningPostProcessorTest.kt | 11 -- ...melineEncryptedHistoryPostProcessorTest.kt | 114 ------------------ ...al_EncryptedHistoryBannerView_Day_0_en.png | 3 - ..._EncryptedHistoryBannerView_Night_0_en.png | 3 - 17 files changed, 3 insertions(+), 306 deletions(-) delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt delete mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/HasEncryptionHistoryBanner.kt delete mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt delete mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Night_0_en.png diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt index a39c2d81e5..7ff428223b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt @@ -16,14 +16,12 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineRoomInfo -import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemReadMarkerView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel -import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel @@ -40,7 +38,6 @@ fun TimelineItemVirtualRow( when (virtual.model) { is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model) TimelineItemReadMarkerModel -> TimelineItemReadMarkerView() - is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView() TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name) is TimelineItemLoadingIndicatorModel -> { TimelineLoadingMoreIndicator(virtual.model.direction) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt deleted file mode 100644 index fecaf85dd8..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.messages.impl.timeline.components.virtual - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -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.messages.impl.R -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Icon - -@Composable -fun TimelineEncryptedHistoryBannerView( - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 32.dp) - .clip(MaterialTheme.shapes.small) - .border(1.dp, ElementTheme.colors.borderInfoSubtle, MaterialTheme.shapes.small) - .background(ElementTheme.colors.bgInfoSubtle) - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = CompoundIcons.InfoSolid(), - contentDescription = null, - tint = ElementTheme.colors.iconInfoPrimary - ) - Text( - text = stringResource(R.string.screen_room_encrypted_history_banner), - style = ElementTheme.typography.fontBodyMdMedium, - color = ElementTheme.colors.textInfoPrimary - ) - } -} - -@PreviewsDayNight -@Composable -internal fun EncryptedHistoryBannerViewPreview() = ElementPreview { - TimelineEncryptedHistoryBannerView() -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt index bced11b56b..8a3c5e2caf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.timeline.factories.virtual import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLastForwardIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel @@ -34,7 +33,6 @@ class TimelineItemVirtualFactory @Inject constructor( return when (val inner = virtual) { is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner) is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel - is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel( direction = inner.direction, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt deleted file mode 100644 index 10e741b676..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.messages.impl.timeline.model.virtual - -data object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel { - override val type: String = "TimelineItemEncryptedHistoryBannerVirtualModel" -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt index f267188bce..98d6d9fc76 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt @@ -16,8 +16,6 @@ sealed interface VirtualTimelineItem { data object ReadMarker : VirtualTimelineItem - data object EncryptedHistoryBanner : VirtualTimelineItem - data object RoomBeginning : VirtualTimelineItem data object LastForwardIndicator : VirtualTimelineItem diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index afdb9e9946..b7fb6d1566 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -87,7 +87,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import org.matrix.rustcomponents.sdk.BackupState import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientException import org.matrix.rustcomponents.sdk.IgnoredUsersListener @@ -179,7 +178,6 @@ class RustMatrixClient( systemClock = clock, roomContentForwarder = RoomContentForwarder(innerRoomListService), roomSyncSubscriber = roomSyncSubscriber, - isKeyBackupEnabled = { client.encryption().use { it.backupState() == BackupState.ENABLED } }, getSessionData = { sessionStore.getSession(sessionId.value)!! }, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 86aaa982d3..1f5ffe7eef 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -89,7 +89,6 @@ import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @OptIn(ExperimentalCoroutinesApi::class) class RustMatrixRoom( override val sessionId: SessionId, - private val isKeyBackupEnabled: Boolean, private val roomListItem: RoomListItem, private val innerRoom: InnerRoom, innerTimeline: InnerTimeline, @@ -652,16 +651,14 @@ class RustMatrixRoom( ): Timeline { val timelineCoroutineScope = roomCoroutineScope.childScope(coroutineDispatchers.main, "TimelineScope-$roomId-$timeline") return RustTimeline( - isKeyBackupEnabled = isKeyBackupEnabled, mode = mode, matrixRoom = this, + inner = timeline, systemClock = systemClock, coroutineScope = timelineCoroutineScope, dispatcher = roomDispatcher, - lastLoginTimestamp = sessionData.loginTimestamp, - onNewSyncedEvent = onNewSyncedEvent, roomContentForwarder = roomContentForwarder, - inner = timeline, + onNewSyncedEvent = onNewSyncedEvent, ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt index 01924b0f92..8db2c498ef 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt @@ -46,7 +46,6 @@ class RustRoomFactory( private val roomListService: RoomListService, private val innerRoomListService: InnerRoomListService, private val roomSyncSubscriber: RoomSyncSubscriber, - private val isKeyBackupEnabled: suspend () -> Boolean, private val getSessionData: suspend () -> SessionData, ) { @OptIn(ExperimentalCoroutinesApi::class) @@ -109,7 +108,6 @@ class RustRoomFactory( val liveTimeline = roomReferences.fullRoom.timeline() RustMatrixRoom( sessionId = sessionId, - isKeyBackupEnabled = isKeyBackupEnabled(), roomListItem = roomReferences.roomListItem, innerRoom = roomReferences.fullRoom, innerTimeline = liveTimeline, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index f349a79ca5..f48af8d3a3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -40,7 +40,6 @@ import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTim import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor -import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper import io.element.android.libraries.matrix.impl.util.MessageEventContent import io.element.android.services.toolbox.api.systemclock.SystemClock @@ -70,7 +69,6 @@ import org.matrix.rustcomponents.sdk.use import timber.log.Timber import uniffi.matrix_sdk_ui.LiveBackPaginationStatus import java.io.File -import java.util.Date import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline private const val PAGINATION_SIZE = 50 @@ -79,11 +77,9 @@ class RustTimeline( private val inner: InnerTimeline, mode: Timeline.Mode, systemClock: SystemClock, - isKeyBackupEnabled: Boolean, private val matrixRoom: MatrixRoom, private val coroutineScope: CoroutineScope, private val dispatcher: CoroutineDispatcher, - lastLoginTimestamp: Date?, private val roomContentForwarder: RoomContentForwarder, onNewSyncedEvent: () -> Unit, ) : Timeline { @@ -107,12 +103,6 @@ class RustTimeline( timelineItems = _timelineItems, timelineItemFactory = timelineItemMapper, ) - private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor( - lastLoginTimestamp = lastLoginTimestamp, - isRoomEncrypted = matrixRoom.isEncrypted, - isKeyBackupEnabled = isKeyBackupEnabled, - dispatcher = dispatcher, - ) private val timelineItemsSubscriber = TimelineItemsSubscriber( timeline = inner, timelineCoroutineScope = coroutineScope, @@ -219,7 +209,6 @@ class RustTimeline( ) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward, isInit -> withContext(dispatcher) { timelineItems - .process { items -> encryptedHistoryPostProcessor.process(items) } .process { items -> roomBeginningPostProcessor.process( items = items, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/HasEncryptionHistoryBanner.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/HasEncryptionHistoryBanner.kt deleted file mode 100644 index 57058a17ea..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/HasEncryptionHistoryBanner.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.timeline.postprocessor - -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem - -internal fun List.hasEncryptionHistoryBanner(): Boolean { - val firstItem = firstOrNull() - return firstItem is MatrixTimelineItem.Virtual && - firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt index 41ac1bbc2a..a4aa6bf05a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/LoadingIndicatorsPostProcessor.kt @@ -19,11 +19,10 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) { hasMoreToLoadBackward: Boolean, hasMoreToLoadForward: Boolean, ): List { - val shouldAddBackwardLoadingIndicator = hasMoreToLoadBackward && !items.hasEncryptionHistoryBanner() val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty() val currentTimestamp = systemClock.epochMillis() return buildList { - if (shouldAddBackwardLoadingIndicator) { + if (hasMoreToLoadBackward) { val backwardLoadingIndicator = MatrixTimelineItem.Virtual( uniqueId = UniqueId("BackwardLoadingIndicator"), virtual = VirtualTimelineItem.LoadingIndicator( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index d6e6ee95a7..2fb41a745c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -36,7 +36,6 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { } private fun processForRoom(items: List): List { - if (items.hasEncryptionHistoryBanner()) return items val roomBeginningItem = createRoomBeginningItem() return listOf(roomBeginningItem) + items } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt deleted file mode 100644 index c9846db104..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.timeline.postprocessor - -import io.element.android.libraries.matrix.api.core.UniqueId -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.util.Date - -internal val encryptedHistoryBannerId = UniqueId("EncryptedHistoryBannerId") - -class TimelineEncryptedHistoryPostProcessor( - private val dispatcher: CoroutineDispatcher, - private val lastLoginTimestamp: Date?, - private val isRoomEncrypted: Boolean, - private val isKeyBackupEnabled: Boolean, -) { - suspend fun process(items: List): List = withContext(dispatcher) { - Timber.d("Process on Thread=${Thread.currentThread()}") - if (!isRoomEncrypted || isKeyBackupEnabled || lastLoginTimestamp == null) return@withContext items - replaceWithEncryptionHistoryBannerIfNeeded(items) - } - - private fun replaceWithEncryptionHistoryBannerIfNeeded(list: List): List { - var lastEncryptedHistoryBannerIndex = -1 - for ((i, item) in list.withIndex()) { - if (isItemEncryptionHistory(item)) { - lastEncryptedHistoryBannerIndex = i - } - } - return if (lastEncryptedHistoryBannerIndex >= 0) { - val sublist = list.drop(lastEncryptedHistoryBannerIndex + 1).toMutableList() - sublist.add(0, MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner)) - sublist - } else { - list - } - } - - private fun isItemEncryptionHistory(item: MatrixTimelineItem): Boolean { - if ((item as? MatrixTimelineItem.Virtual)?.virtual is VirtualTimelineItem.EncryptedHistoryBanner) { - return true - } - val timestamp = (item as? MatrixTimelineItem.Event)?.event?.timestamp ?: return false - return timestamp <= lastLoginTimestamp!!.time - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt index 8040a5f18d..db93b86142 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt @@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent -import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.timeline.aMessageContent @@ -70,16 +69,6 @@ class RoomBeginningPostProcessorTest { ) } - @Test - fun `processor will not add beginning of room item if it's not a DM and EncryptedHistoryBanner item is found`() { - val timelineItems = listOf( - MatrixTimelineItem.Virtual(UniqueId("EncryptedHistoryBanner"), VirtualTimelineItem.EncryptedHistoryBanner), - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE) - val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false) - assertThat(processedItems).isEqualTo(timelineItems) - } - @Test fun `processor won't remove items if it's not at the start of the timeline`() { val timelineItems = listOf( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt deleted file mode 100644 index 0d80bdec46..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.libraries.matrix.impl.timeline.postprocessor - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem -import io.element.android.libraries.matrix.test.A_UNIQUE_ID -import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.util.Date - -class TimelineEncryptedHistoryPostProcessorTest { - private val defaultLastLoginTimestamp = Date(1_689_061_264L) - - @Test - fun `given an unencrypted room, nothing is done`() = runTest { - val processor = createPostProcessor(isRoomEncrypted = false) - val items = listOf( - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()) - ) - assertThat(processor.process(items)).isSameInstanceAs(items) - } - - @Test - fun `given an encrypted room, and key backup enabled, nothing is done`() = runTest { - val processor = createPostProcessor(isKeyBackupEnabled = true) - val items = listOf( - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()) - ) - assertThat(processor.process(items)).isSameInstanceAs(items) - } - - @Test - fun `given a null lastLoginTimestamp, nothing is done`() = runTest { - val processor = createPostProcessor(lastLoginTimestamp = null) - val items = listOf( - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()) - ) - assertThat(processor.process(items)).isSameInstanceAs(items) - } - - @Test - fun `given an empty list, nothing is done`() = runTest { - val processor = createPostProcessor() - val items = emptyList() - assertThat(processor.process(items)).isSameInstanceAs(items) - } - - @Test - fun `given a list with no items before lastLoginTimestamp, nothing is done`() = runTest { - val processor = createPostProcessor() - val items = listOf( - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)) - ) - assertThat(processor.process(items)).isSameInstanceAs(items) - } - - @Test - fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() = runTest { - val processor = createPostProcessor() - val items = listOf( - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)) - ) - assertThat(processor.process(items)) - .isEqualTo(listOf(MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner))) - } - - @Test - fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() = runTest { - val processor = createPostProcessor() - val items = listOf( - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)) - ) - assertThat(processor.process(items)).isEqualTo( - listOf(MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner)) - ) - } - - @Test - fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, then they're replaced`() = runTest { - val processor = createPostProcessor() - val items = listOf( - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)), - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)), - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)), - ) - assertThat(processor.process(items)).isEqualTo( - listOf( - MatrixTimelineItem.Virtual(encryptedHistoryBannerId, VirtualTimelineItem.EncryptedHistoryBanner), - MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)) - ) - ) - } - - private fun TestScope.createPostProcessor( - lastLoginTimestamp: Date? = defaultLastLoginTimestamp, - isRoomEncrypted: Boolean = true, - isKeyBackupEnabled: Boolean = false, - ) = TimelineEncryptedHistoryPostProcessor( - lastLoginTimestamp = lastLoginTimestamp, - isRoomEncrypted = isRoomEncrypted, - isKeyBackupEnabled = isKeyBackupEnabled, - dispatcher = StandardTestDispatcher(testScheduler) - ) -} diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Day_0_en.png deleted file mode 100644 index 0e2296e251..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:47c6681e3cf56d3229953b7c34b6f1d531c492d23f4dffe77e2a12fbdbf44261 -size 11892 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Night_0_en.png deleted file mode 100644 index 70416e47dc..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:985b87b568e34b16d132a5cde5781e99edc81be4ef53c5b6e67ca75472266f99 -size 11436 From 39c296efc62de5c7df2b2bef2f37dd66edb7d386 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 9 Sep 2024 14:04:29 +0200 Subject: [PATCH 05/12] Remove Wait list screen #3293 --- .../features/login/impl/LoginFlowNode.kt | 24 +-- .../login/impl/error/WaitListError.kt | 14 -- .../loginpassword/LoginPasswordNode.kt | 10 -- .../loginpassword/LoginPasswordView.kt | 16 +- .../screens/waitlistscreen/WaitListEvents.kt | 14 -- .../screens/waitlistscreen/WaitListNode.kt | 52 ------- .../waitlistscreen/WaitListPresenter.kt | 87 ----------- .../screens/waitlistscreen/WaitListState.kt | 19 --- .../waitlistscreen/WaitListStateProvider.kt | 35 ----- .../screens/waitlistscreen/WaitListView.kt | 143 ------------------ .../waitlistscreen/WaitListPresenterTest.kt | 114 -------------- .../android/samples/minimal/LoginScreen.kt | 1 - ...s.waitlistscreen_WaitListView_Day_0_de.png | 3 - ...s.waitlistscreen_WaitListView_Day_1_de.png | 3 - ...s.waitlistscreen_WaitListView_Day_2_de.png | 3 - ...s.waitlistscreen_WaitListView_Day_3_de.png | 3 - ...s.waitlistscreen_WaitListView_Day_4_de.png | 3 - ...s.waitlistscreen_WaitListView_Day_0_en.png | 3 - ...s.waitlistscreen_WaitListView_Day_1_en.png | 3 - ...s.waitlistscreen_WaitListView_Day_2_en.png | 3 - ...s.waitlistscreen_WaitListView_Day_3_en.png | 3 - ...s.waitlistscreen_WaitListView_Day_4_en.png | 3 - ...waitlistscreen_WaitListView_Night_0_en.png | 3 - ...waitlistscreen_WaitListView_Night_1_en.png | 3 - ...waitlistscreen_WaitListView_Night_2_en.png | 3 - ...waitlistscreen_WaitListView_Night_3_en.png | 3 - ...waitlistscreen_WaitListView_Night_4_en.png | 3 - 27 files changed, 4 insertions(+), 570 deletions(-) delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/WaitListError.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListNode.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListState.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt delete mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt delete mode 100644 screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_0_de.png delete mode 100644 screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_1_de.png delete mode 100644 screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_2_de.png delete mode 100644 screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_3_de.png delete mode 100644 screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_4_de.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_1_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_2_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_4_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_1_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_2_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_4_en.png diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index eae2cfba17..a118920ebd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -19,7 +19,6 @@ 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.newRoot import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.singleTop import dagger.assisted.Assisted @@ -31,10 +30,8 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderDat import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode -import io.element.android.features.login.impl.screens.loginpassword.LoginFormState import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode -import io.element.android.features.login.impl.screens.waitlistscreen.WaitListNode import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.NodeInputs @@ -112,9 +109,6 @@ class LoginFlowNode @AssistedInject constructor( @Parcelize data object LoginPassword : NavTarget - @Parcelize - data class WaitList(val loginFormState: LoginFormState) : NavTarget - @Parcelize data class OidcView(val oidcDetails: OidcDetails) : NavTarget } @@ -181,27 +175,11 @@ class LoginFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(callback)) } NavTarget.LoginPassword -> { - val callback = object : LoginPasswordNode.Callback { - override fun onWaitListError(loginFormState: LoginFormState) { - backstack.newRoot(NavTarget.WaitList(loginFormState)) - } - } - createNode(buildContext, plugins = listOf(callback)) + createNode(buildContext) } is NavTarget.OidcView -> { oidcEntryPoint.createFallbackWebViewNode(this, buildContext, navTarget.oidcDetails.url) } - is NavTarget.WaitList -> { - val inputs = WaitListNode.Inputs( - loginFormState = navTarget.loginFormState, - ) - val callback = object : WaitListNode.Callback { - override fun onCancelClick() { - navigateUp() - } - } - createNode(buildContext, plugins = listOf(callback, inputs)) - } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/WaitListError.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/WaitListError.kt deleted file mode 100644 index d4bd1e323c..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/WaitListError.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.login.impl.error - -import io.element.android.libraries.core.bool.orFalse - -fun Throwable.isWaitListError(): Boolean { - return message?.contains("IO_ELEMENT_X_WAIT_LIST").orFalse() -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt index 7dcb22790e..ebd2b3a5be 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt @@ -12,7 +12,6 @@ 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 dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -24,14 +23,6 @@ class LoginPasswordNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: LoginPasswordPresenter, ) : Node(buildContext, plugins = plugins) { - interface Callback : Plugin { - fun onWaitListError(loginFormState: LoginFormState) - } - - private fun onWaitListError(loginFormState: LoginFormState) { - plugins().forEach { it.onWaitListError(loginFormState) } - } - @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -39,7 +30,6 @@ class LoginPasswordNode @AssistedInject constructor( state = state, modifier = modifier, onBackClick = ::navigateUp, - onWaitListError = ::onWaitListError, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index 9a7e3df3f0..bd6437ddba 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -44,7 +44,6 @@ 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.login.impl.R -import io.element.android.features.login.impl.error.isWaitListError import io.element.android.features.login.impl.error.loginError import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule @@ -72,7 +71,6 @@ import io.element.android.libraries.ui.strings.CommonStrings fun LoginPasswordView( state: LoginPasswordState, onBackClick: () -> Unit, - onWaitListError: (LoginFormState) -> Unit, modifier: Modifier = Modifier, ) { val isLoading by remember(state.loginAction) { @@ -149,16 +147,9 @@ fun LoginPasswordView( } if (state.loginAction is AsyncData.Failure) { - when { - state.loginAction.error.isWaitListError() -> { - onWaitListError(state.formState) - } - else -> { - LoginErrorDialog(error = state.loginAction.error, onDismiss = { - state.eventSink(LoginPasswordEvents.ClearError) - }) - } - } + LoginErrorDialog(error = state.loginAction.error, onDismiss = { + state.eventSink(LoginPasswordEvents.ClearError) + }) } } } @@ -302,6 +293,5 @@ internal fun LoginPasswordViewPreview(@PreviewParameter(LoginPasswordStateProvid LoginPasswordView( state = state, onBackClick = {}, - onWaitListError = {}, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt deleted file mode 100644 index 4cdfb206a8..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListEvents.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.waitlistscreen - -sealed interface WaitListEvents { - data object AttemptLogin : WaitListEvents - data object ClearError : WaitListEvents - data object Continue : WaitListEvents -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListNode.kt deleted file mode 100644 index 8f020d2231..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListNode.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.waitlistscreen - -import androidx.compose.runtime.Composable -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 dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.login.impl.screens.loginpassword.LoginFormState -import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope - -@ContributesNode(AppScope::class) -class WaitListNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: WaitListPresenter.Factory, -) : Node(buildContext, plugins = plugins) { - data class Inputs(val loginFormState: LoginFormState) : NodeInputs - - private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create(inputs.loginFormState) - - interface Callback : Plugin { - fun onCancelClick() - } - - private fun onCancelClick() { - plugins().forEach { it.onCancelClick() } - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - WaitListView( - state = state, - onCancelClick = ::onCancelClick, - modifier = modifier - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt deleted file mode 100644 index 5c3a6fa530..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenter.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.waitlistscreen - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.element.android.features.login.impl.DefaultLoginUserStory -import io.element.android.features.login.impl.screens.loginpassword.LoginFormState -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.core.SessionId -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import timber.log.Timber - -class WaitListPresenter @AssistedInject constructor( - @Assisted private val formState: LoginFormState, - private val buildMeta: BuildMeta, - private val authenticationService: MatrixAuthenticationService, - private val defaultLoginUserStory: DefaultLoginUserStory, -) : Presenter { - @AssistedFactory - interface Factory { - fun create(loginFormState: LoginFormState): WaitListPresenter - } - - @Composable - override fun present(): WaitListState { - val coroutineScope = rememberCoroutineScope() - val homeserverUrl = remember { - authenticationService.getHomeserverDetails().value?.url ?: "server" - } - - val loginAction: MutableState> = remember { - mutableStateOf(AsyncData.Uninitialized) - } - - val attemptNumber = remember { mutableIntStateOf(0) } - - fun handleEvents(event: WaitListEvents) { - when (event) { - WaitListEvents.AttemptLogin -> { - // Do not attempt to login on first resume of the View. - attemptNumber.intValue++ - if (attemptNumber.intValue > 1) { - coroutineScope.loginAttempt(formState, loginAction) - } - } - WaitListEvents.ClearError -> loginAction.value = AsyncData.Uninitialized - WaitListEvents.Continue -> defaultLoginUserStory.setLoginFlowIsDone(true) - } - } - - return WaitListState( - appName = buildMeta.applicationName, - serverName = homeserverUrl, - loginAction = loginAction.value, - eventSink = ::handleEvents - ) - } - - private fun CoroutineScope.loginAttempt(formState: LoginFormState, loggedInState: MutableState>) = launch { - Timber.w("Attempt to login...") - loggedInState.value = AsyncData.Loading() - authenticationService.login(formState.login.trim(), formState.password) - .onSuccess { sessionId -> - loggedInState.value = AsyncData.Success(sessionId) - } - .onFailure { failure -> - loggedInState.value = AsyncData.Failure(failure) - } - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListState.kt deleted file mode 100644 index 34c0fb2e65..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListState.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.waitlistscreen - -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.core.SessionId - -// Do not use default value, so no member get forgotten in the presenters. -data class WaitListState( - val appName: String, - val serverName: String, - val loginAction: AsyncData, - val eventSink: (WaitListEvents) -> Unit -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt deleted file mode 100644 index 93d70a5098..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListStateProvider.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.waitlistscreen - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.core.SessionId - -open class WaitListStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aWaitListState(loginAction = AsyncData.Uninitialized), - aWaitListState(loginAction = AsyncData.Loading()), - aWaitListState(loginAction = AsyncData.Failure(Throwable("error"))), - aWaitListState(loginAction = AsyncData.Failure(Throwable(message = "IO_ELEMENT_X_WAIT_LIST"))), - aWaitListState(loginAction = AsyncData.Success(SessionId("@alice:element.io"))), - // Add other state here - ) -} - -fun aWaitListState( - appName: String = "Element X", - serverName: String = "server.org", - loginAction: AsyncData = AsyncData.Uninitialized, -) = WaitListState( - appName = appName, - serverName = serverName, - loginAction = loginAction, - eventSink = {} -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt deleted file mode 100644 index 2ca73e77e4..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListView.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.waitlistscreen - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.LocalContentColor -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -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 androidx.lifecycle.Lifecycle -import io.element.android.compound.theme.ElementTheme -import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.error.isWaitListError -import io.element.android.features.login.impl.error.loginError -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.atomic.pages.SunsetPage -import io.element.android.libraries.designsystem.components.dialogs.RetryDialog -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Button -import io.element.android.libraries.designsystem.theme.components.TextButton -import io.element.android.libraries.designsystem.utils.OnLifecycleEvent -import io.element.android.libraries.ui.strings.CommonStrings - -// Ref: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=6761-148425 -// Only the first screen can be displayed, since once logged in, this Node will be remove by the RootNode. -@Composable -fun WaitListView( - state: WaitListState, - onCancelClick: () -> Unit, - modifier: Modifier = Modifier, -) { - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(WaitListEvents.AttemptLogin) - else -> Unit - } - } - WaitListContent(state, onCancelClick, modifier) -} - -@Composable -private fun WaitListError(state: WaitListState) { - // Display a dialog for error other than the waitlist error - state.loginAction.errorOrNull()?.let { error -> - if (error.isWaitListError().not()) { - RetryDialog( - content = stringResource(id = loginError(error)), - onRetry = { - state.eventSink.invoke(WaitListEvents.AttemptLogin) - }, - onDismiss = { - state.eventSink.invoke(WaitListEvents.ClearError) - } - ) - } - } -} - -@Composable -private fun WaitListContent( - state: WaitListState, - onCancelClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier.fillMaxSize(), - ) { - val title = stringResource( - when (state.loginAction) { - is AsyncData.Success -> R.string.screen_waitlist_title_success - else -> R.string.screen_waitlist_title - } - ) - val subtitle = when (state.loginAction) { - is AsyncData.Success -> stringResource( - id = R.string.screen_waitlist_message_success, - state.appName, - ) - else -> stringResource( - id = R.string.screen_waitlist_message, - state.appName, - state.serverName, - ) - } - SunsetPage( - isLoading = state.loginAction.isLoading(), - title = title, - subtitle = subtitle, - ) { - OverallContent(state, onCancelClick) - } - WaitListError(state) - } -} - -@Composable -private fun OverallContent( - state: WaitListState, - onCancelClick: () -> Unit, -) { - Box(modifier = Modifier.fillMaxSize()) { - if (state.loginAction !is AsyncData.Success) { - CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textOnSolidPrimary) { - TextButton( - text = stringResource(CommonStrings.action_cancel), - onClick = onCancelClick, - ) - } - } - if (state.loginAction is AsyncData.Success) { - Button( - text = stringResource(id = CommonStrings.action_continue), - onClick = { state.eventSink.invoke(WaitListEvents.Continue) }, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(bottom = 8.dp), - ) - } - } -} - -@PreviewsDayNight -@Composable -internal fun WaitListViewPreview(@PreviewParameter(WaitListStateProvider::class) state: WaitListState) = ElementPreview { - WaitListView( - state = state, - onCancelClick = {}, - ) -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt deleted file mode 100644 index aded55aa3d..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/waitlistscreen/WaitListPresenterTest.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.login.impl.screens.waitlistscreen - -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.impl.DefaultLoginUserStory -import io.element.android.features.login.impl.screens.loginpassword.LoginFormState -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.test.A_HOMESERVER -import io.element.android.libraries.matrix.test.A_HOMESERVER_URL -import io.element.android.libraries.matrix.test.A_THROWABLE -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService -import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.tests.testutils.WarmUpRule -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class WaitListPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val authenticationService = FakeMatrixAuthenticationService().apply { - givenHomeserver(A_HOMESERVER) - } - val loginUserStory = DefaultLoginUserStory() - val presenter = WaitListPresenter( - LoginFormState.Default, - aBuildMeta(applicationName = "Application Name"), - authenticationService, - loginUserStory, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.appName).isEqualTo("Application Name") - assertThat(initialState.serverName).isEqualTo(A_HOMESERVER_URL) - assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized) - } - } - - @Test - fun `present - attempt login with error`() = runTest { - val authenticationService = FakeMatrixAuthenticationService().apply { - givenLoginError(A_THROWABLE) - } - val loginUserStory = DefaultLoginUserStory() - val presenter = WaitListPresenter( - LoginFormState.Default, - aBuildMeta(), - authenticationService, - loginUserStory, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - // First usage of AttemptLogin, nothing should happen - initialState.eventSink.invoke(WaitListEvents.AttemptLogin) - expectNoEvents() - initialState.eventSink.invoke(WaitListEvents.AttemptLogin) - val submitState = awaitItem() - assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java) - val errorState = awaitItem() - assertThat(errorState.loginAction).isEqualTo(AsyncData.Failure(A_THROWABLE)) - // Assert the error can be cleared - errorState.eventSink(WaitListEvents.ClearError) - val clearedState = awaitItem() - assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized) - } - } - - @Test - fun `present - attempt login with success`() = runTest { - val authenticationService = FakeMatrixAuthenticationService() - val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) } - val presenter = WaitListPresenter( - LoginFormState.Default, - aBuildMeta(), - authenticationService, - loginUserStory, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - assertThat(loginUserStory.loginFlowIsDone.value).isFalse() - val initialState = awaitItem() - // First usage of AttemptLogin, nothing should happen - initialState.eventSink.invoke(WaitListEvents.AttemptLogin) - expectNoEvents() - initialState.eventSink.invoke(WaitListEvents.AttemptLogin) - val submitState = awaitItem() - assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java) - val successState = awaitItem() - assertThat(successState.loginAction).isEqualTo(AsyncData.Success(A_USER_ID)) - assertThat(loginUserStory.loginFlowIsDone.value).isFalse() - successState.eventSink.invoke(WaitListEvents.Continue) - assertThat(loginUserStory.loginFlowIsDone.value).isTrue() - } - } -} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt index 85c8541aa5..1e4324d99f 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt @@ -38,7 +38,6 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService state = state, modifier = modifier, onBackClick = {}, - onWaitListError = {}, ) } } diff --git a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_0_de.png b/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_0_de.png deleted file mode 100644 index bdbf2083a0..0000000000 --- a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_0_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b2c858bc7e2beecc0d4c92df0b4ac61e1e3a975a072e0e75cfa1da6aaa32142c -size 140926 diff --git a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_1_de.png b/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_1_de.png deleted file mode 100644 index 41cb33681f..0000000000 --- a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_1_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8524bdd37ac6eb784d82041b572d9cf3bb69cf3de318ba1d8abc45e9a239dad1 -size 141726 diff --git a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_2_de.png b/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_2_de.png deleted file mode 100644 index 7e93f91038..0000000000 --- a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_2_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4fd2455de9a763e582e4977e36686c714dfb380737fc6193b4eab9ef8b64de9 -size 58641 diff --git a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_3_de.png b/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_3_de.png deleted file mode 100644 index bdbf2083a0..0000000000 --- a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_3_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b2c858bc7e2beecc0d4c92df0b4ac61e1e3a975a072e0e75cfa1da6aaa32142c -size 140926 diff --git a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_4_de.png b/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_4_de.png deleted file mode 100644 index 3dbb9b5077..0000000000 --- a/screenshots/de/features.login.impl.screens.waitlistscreen_WaitListView_Day_4_de.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f7880a67dd818c44b519be634a4a33a4a0efc842f9ad5e4879f16acb7c8b1f5f -size 122622 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_0_en.png deleted file mode 100644 index 305577539c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa9162336cb7d46a2517f4b4149a9f8389e69e410e3896cb6b973fc16a940adf -size 138058 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_1_en.png deleted file mode 100644 index 4c6e3b76f0..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ef852707489db29300b4bf8259a5d71d3c82ce26bcea3ddb00d037b1ba379423 -size 138901 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_2_en.png deleted file mode 100644 index c057952aad..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5946208785bcc7eff5e76048cd467e9bde6f4ff5874ef2dd2562d5bbb0844f93 -size 59177 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_3_en.png deleted file mode 100644 index 305577539c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa9162336cb7d46a2517f4b4149a9f8389e69e410e3896cb6b973fc16a940adf -size 138058 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_4_en.png deleted file mode 100644 index f87c8c0c03..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Day_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b4fe8418f426d9953189908053407550ad6d83d45a548253d75507321f686c9 -size 120721 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_0_en.png deleted file mode 100644 index 87a7924841..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d81ad9f5a5983653d9266a2500b2c71dc7ca231d52a62d1c1f1b091849ede32 -size 165501 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_1_en.png deleted file mode 100644 index d3cd2906ba..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1cb4f2cbdc2b7fca643a9debff50206d4aa9321894a86149d6c815fd4db7b593 -size 166084 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_2_en.png deleted file mode 100644 index e2167c3dcf..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b942ce79745a26aa2d4227c35aaad442b35c2f768915e3d9fabf45a04cd1d82 -size 61267 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_3_en.png deleted file mode 100644 index 87a7924841..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d81ad9f5a5983653d9266a2500b2c71dc7ca231d52a62d1c1f1b091849ede32 -size 165501 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_4_en.png deleted file mode 100644 index a283905f3b..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.waitlistscreen_WaitListView_Night_4_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aa47e394a5af6cad06496a5d36fa81b2e7d76dd9eaec45c3914f1ffce45f9f91 -size 146617 From 8154aa3319e88c24f69120210a9cdd3b788e4091 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 9 Sep 2024 18:13:19 +0200 Subject: [PATCH 06/12] Add banner for optional migration to simplified sliding sync (#3429) * Add banner for optional migration to native sliding sync - Add `MatrixClient.isNativeSlidingSyncSupported()` and `MatrixClient.isUsingNativeSlidingSync` to check whether the home server supports native sliding sync and we're already using it. - Add `NativeSlidingSyncMigrationBanner` composable to the `RoomList` screen when the home server supports native sliding sync but the current session is not using it. - Add an extra logout successful action to the logout flow, create `EnableNativeSlidingSyncUseCase` so it can be used there. * Update screenshots * Make sure the sliding sync migration banner has lower priority than the encryption setup ones --------- Co-authored-by: ElementBot --- .../android/appnav/LoggedInFlowNode.kt | 25 +++++++++++ .../features/logout/api/LogoutEntryPoint.kt | 1 + .../logout/impl/DefaultLogoutEntryPoint.kt | 9 ++++ .../features/logout/impl/LogoutNode.kt | 11 ++++- .../roomlist/api/RoomListEntryPoint.kt | 1 + features/roomlist/impl/build.gradle.kts | 2 + .../impl/RoomListContentStateProvider.kt | 1 + .../features/roomlist/impl/RoomListEvents.kt | 2 +- .../features/roomlist/impl/RoomListNode.kt | 17 ++++++++ .../roomlist/impl/RoomListPresenter.kt | 14 ++++++- .../features/roomlist/impl/RoomListState.kt | 3 ++ .../roomlist/impl/RoomListStateProvider.kt | 4 ++ .../features/roomlist/impl/RoomListView.kt | 5 +++ .../NativeSlidingSyncMigrationBanner.kt | 41 +++++++++++++++++++ .../impl/components/RoomListContentView.kt | 18 +++++++- .../impl/src/main/res/values/localazy.xml | 3 ++ .../roomlist/impl/RoomListPresenterTest.kt | 12 +++++- .../roomlist/impl/RoomListViewTest.kt | 21 +++++++++- .../molecules/DialogLikeBannerMolecule.kt | 3 +- .../libraries/matrix/api/MatrixClient.kt | 6 +++ .../libraries/matrix/impl/RustMatrixClient.kt | 9 ++++ .../libraries/matrix/test/FakeMatrixClient.kt | 10 +++++ libraries/preferences/api/build.gradle.kts | 4 ++ .../store/EnableNativeSlidingSyncUseCase.kt | 23 +++++++++++ .../EnableNativeSlidingSyncUseCaseTest.kt | 31 ++++++++++++++ samples/minimal/build.gradle.kts | 1 + .../android/samples/minimal/RoomListScreen.kt | 5 ++- ...iveSlidingSyncMigrationBanner_Day_0_en.png | 3 ++ ...eSlidingSyncMigrationBanner_Night_0_en.png | 3 ++ ...omponents_RoomListContentView_Day_4_en.png | 3 ++ ...ponents_RoomListContentView_Night_4_en.png | 3 ++ tools/localazy/config.json | 1 + 32 files changed, 283 insertions(+), 12 deletions(-) create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt create mode 100644 libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt create mode 100644 libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index db153314d6..5a8db185ef 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -40,6 +40,7 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState +import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint @@ -65,6 +66,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.sync.SyncState +import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview @@ -96,6 +98,8 @@ class LoggedInFlowNode @AssistedInject constructor( private val shareEntryPoint: ShareEntryPoint, private val matrixClient: MatrixClient, private val sendingQueue: SendQueues, + private val logoutEntryPoint: LogoutEntryPoint, + private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase, snackbarDispatcher: SnackbarDispatcher, ) : BaseFlowNode( backstack = BackStack( @@ -225,6 +229,9 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data class IncomingShare(val intent: Intent) : NavTarget + + @Parcelize + data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -271,6 +278,10 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onRoomDirectorySearchClick() { backstack.push(NavTarget.RoomDirectorySearch) } + + override fun onLogoutForNativeSlidingSyncMigrationNeeded() { + backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded) + } } roomListEntryPoint .nodeBuilder(this, buildContext) @@ -407,6 +418,20 @@ class LoggedInFlowNode @AssistedInject constructor( .params(ShareEntryPoint.Params(intent = navTarget.intent)) .build() } + is NavTarget.LogoutForNativeSlidingSyncMigrationNeeded -> { + val callback = object : LogoutEntryPoint.Callback { + override fun onChangeRecoveryKeyClick() { + backstack.push(NavTarget.SecureBackup()) + } + } + + logoutEntryPoint.nodeBuilder(this, buildContext) + .onSuccessfulLogoutPendingAction { + enableNativeSlidingSyncUseCase() + } + .callback(callback) + .build() + } } } diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt index d90f039ec1..ea952b0100 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt @@ -16,6 +16,7 @@ interface LogoutEntryPoint : FeatureEntryPoint { fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { + fun onSuccessfulLogoutPendingAction(action: () -> Unit): NodeBuilder fun callback(callback: Callback): NodeBuilder fun build(): Node } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt index 7c7264a00f..08de15c2ee 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt @@ -27,6 +27,15 @@ class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint { return this } + override fun onSuccessfulLogoutPendingAction(action: () -> Unit): LogoutEntryPoint.NodeBuilder { + plugins += object : LogoutNode.SuccessfulLogoutPendingAction, Plugin { + override fun onSuccessfulLogoutPendingAction() { + action() + } + } + return this + } + override fun build(): Node { return parentNode.createNode(buildContext, plugins) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt index fb2c1585a0..dd9788cd1d 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt @@ -33,6 +33,12 @@ class LogoutNode @AssistedInject constructor( plugins().forEach { it.onChangeRecoveryKeyClick() } } + interface SuccessfulLogoutPendingAction : Plugin { + fun onSuccessfulLogoutPendingAction() + } + + private val customOnSuccessfulLogoutPendingAction = plugins().firstOrNull() + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -41,7 +47,10 @@ class LogoutNode @AssistedInject constructor( LogoutView( state = state, onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick, - onSuccessLogout = { onSuccessLogout(activity, isDark, it) }, + onSuccessLogout = { + customOnSuccessfulLogoutPendingAction?.onSuccessfulLogoutPendingAction() + onSuccessLogout(activity, isDark, it) + }, onBackClick = ::navigateUp, modifier = modifier, ) diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt index 7070fbe915..98ed717f7b 100644 --- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt +++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt @@ -29,5 +29,6 @@ interface RoomListEntryPoint : FeatureEntryPoint { fun onRoomSettingsClick(roomId: RoomId) fun onReportBugClick() fun onRoomDirectorySearchClick() + fun onLogoutForNativeSlidingSyncMigrationNeeded() } } diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 5e3dc513c6..ce747859f5 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(projects.libraries.push.api) implementation(projects.features.invite.api) implementation(projects.features.networkmonitor.api) + implementation(projects.features.logout.api) implementation(projects.features.leaveroom.api) implementation(projects.services.analytics.api) implementation(libs.androidx.datastore.preferences) @@ -75,6 +76,7 @@ dependencies { testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) testImplementation(projects.features.networkmonitor.test) + testImplementation(projects.features.logout.test) testImplementation(projects.tests.testutils) testImplementation(projects.features.leaveroom.test) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt index 4e586056fd..6656ca8231 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContentStateProvider.kt @@ -20,6 +20,7 @@ open class RoomListContentStateProvider : PreviewParameterProvider().forEach { it.onLogoutForNativeSlidingSyncMigrationNeeded() } + } + }, modifier = modifier, ) { acceptDeclineInviteView.Render( @@ -107,5 +120,9 @@ class RoomListNode @AssistedInject constructor( modifier = Modifier ) } + + directLogoutView.Render(state.directLogoutState) { + enableNativeSlidingSyncUseCase() + } } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index d2b45c0d06..2983b6b695 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -29,6 +29,7 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.InviteData import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter +import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.roomlist.impl.datasource.RoomListDataSource @@ -88,6 +89,7 @@ class RoomListPresenter @Inject constructor( private val acceptDeclineInvitePresenter: Presenter, private val fullScreenIntentPermissionsPresenter: FullScreenIntentPermissionsPresenter, private val notificationCleaner: NotificationCleaner, + private val logoutPresenter: DirectLogoutPresenter, ) : Presenter { private val encryptionService: EncryptionService = client.encryptionService() private val syncService: SyncService = client.syncService() @@ -115,13 +117,15 @@ class RoomListPresenter @Inject constructor( val contextMenu = remember { mutableStateOf(RoomListState.ContextMenu.Hidden) } + val directLogoutState = logoutPresenter.present() + fun handleEvents(event: RoomListEvents) { when (event) { is RoomListEvents.UpdateVisibleRange -> coroutineScope.launch { updateVisibleRange(event.range) } RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true - RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true + RoomListEvents.DismissBanner -> securityBannerDismissed = true RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility) is RoomListEvents.ShowContextMenu -> { coroutineScope.showContextMenu(event, contextMenu) @@ -161,6 +165,7 @@ class RoomListPresenter @Inject constructor( searchState = searchState, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, + directLogoutState = directLogoutState, eventSink = ::handleEvents, ) } @@ -168,6 +173,7 @@ class RoomListPresenter @Inject constructor( @Composable private fun securityBannerState( securityBannerDismissed: Boolean, + needsSlidingSyncMigration: Boolean, ): State { val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed) val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() @@ -185,6 +191,7 @@ class RoomListPresenter @Inject constructor( RecoveryState.ENABLED -> SecurityBannerState.None } } + needsSlidingSyncMigration -> SecurityBannerState.NeedsNativeSlidingSyncMigration else -> SecurityBannerState.None } } @@ -209,11 +216,14 @@ class RoomListPresenter @Inject constructor( loadingState == RoomList.LoadingState.NotLoaded || roomSummaries is AsyncData.Loading } } + val needsSlidingSyncMigration by produceState(false) { + value = client.isNativeSlidingSyncSupported() && !client.isUsingNativeSlidingSync() + } return when { showEmpty -> RoomListContentState.Empty showSkeleton -> RoomListContentState.Skeleton(count = 16) else -> { - val securityBannerState by securityBannerState(securityBannerDismissed) + val securityBannerState by securityBannerState(securityBannerDismissed, needsSlidingSyncMigration) RoomListContentState.Rooms( securityBannerState = securityBannerState, fullScreenIntentPermissionsState = fullScreenIntentPermissionsPresenter.present(), diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index bb95b85293..ae9a721bfa 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -10,6 +10,7 @@ package io.element.android.features.roomlist.impl import androidx.compose.runtime.Immutable import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomState +import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.search.RoomListSearchState @@ -31,6 +32,7 @@ data class RoomListState( val searchState: RoomListSearchState, val contentState: RoomListContentState, val acceptDeclineInviteState: AcceptDeclineInviteState, + val directLogoutState: DirectLogoutState, val eventSink: (RoomListEvents) -> Unit, ) { val displayFilters = contentState is RoomListContentState.Rooms @@ -59,6 +61,7 @@ enum class SecurityBannerState { None, SetUpRecovery, RecoveryKeyConfirmation, + NeedsNativeSlidingSyncMigration, } @Immutable diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 31499600ba..b9d003e32c 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -12,6 +12,8 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState +import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary @@ -57,6 +59,7 @@ internal fun aRoomListState( filtersState: RoomListFiltersState = aRoomListFiltersState(), contentState: RoomListContentState = aRoomsContentState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), + directLogoutState: DirectLogoutState = aDirectLogoutState(), eventSink: (RoomListEvents) -> Unit = {} ) = RoomListState( matrixUser = matrixUser, @@ -69,6 +72,7 @@ internal fun aRoomListState( searchState = searchState, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, + directLogoutState = directLogoutState, eventSink = eventSink, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index cfa5605c1e..4db0f68e4e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -50,6 +50,7 @@ fun RoomListView( onRoomSettingsClick: (roomId: RoomId) -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, onRoomDirectorySearchClick: () -> Unit, + onMigrateToNativeSlidingSyncClick: () -> Unit, modifier: Modifier = Modifier, acceptDeclineInviteView: @Composable () -> Unit, ) { @@ -76,6 +77,7 @@ fun RoomListView( onOpenSettings = onSettingsClick, onCreateRoomClick = onCreateRoomClick, onMenuActionClick = onMenuActionClick, + onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, modifier = Modifier.padding(top = topPadding), ) // This overlaid view will only be visible when state.displaySearchResults is true @@ -105,6 +107,7 @@ private fun RoomListScaffold( onOpenSettings: () -> Unit, onCreateRoomClick: () -> Unit, onMenuActionClick: (RoomListMenuAction) -> Unit, + onMigrateToNativeSlidingSyncClick: () -> Unit, modifier: Modifier = Modifier, ) { fun onRoomClick(room: RoomListRoomSummary) { @@ -140,6 +143,7 @@ private fun RoomListScaffold( onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onRoomClick = ::onRoomClick, onCreateRoomClick = onCreateRoomClick, + onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, modifier = Modifier .padding(padding) .consumeWindowInsets(padding) @@ -180,5 +184,6 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class) onMenuActionClick = {}, onRoomDirectorySearchClick = {}, acceptDeclineInviteView = {}, + onMigrateToNativeSlidingSyncClick = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt new file mode 100644 index 0000000000..6d058d6a8a --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/NativeSlidingSyncMigrationBanner.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.roomlist.impl.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.features.roomlist.impl.R +import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@Composable +internal fun NativeSlidingSyncMigrationBanner( + onContinueClick: () -> Unit, + onDismissClick: () -> Unit, + modifier: Modifier = Modifier, +) { + DialogLikeBannerMolecule( + modifier = modifier, + title = stringResource(R.string.banner_migrate_to_native_sliding_sync_title), + content = stringResource(R.string.banner_migrate_to_native_sliding_sync_description), + actionText = stringResource(R.string.banner_migrate_to_native_sliding_sync_action), + onSubmitClick = onContinueClick, + onDismissClick = onDismissClick, + ) +} + +@PreviewsDayNight +@Composable +internal fun NativeSlidingSyncMigrationBannerPreview() = ElementPreview { + NativeSlidingSyncMigrationBanner( + onContinueClick = {}, + onDismissClick = {}, + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt index 8abbce3068..d4af5a3fe5 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListContentView.kt @@ -64,6 +64,7 @@ fun RoomListContentView( onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, onCreateRoomClick: () -> Unit, + onMigrateToNativeSlidingSyncClick: () -> Unit, modifier: Modifier = Modifier, ) { Box(modifier = modifier) { @@ -85,6 +86,7 @@ fun RoomListContentView( eventSink = eventSink, onSetUpRecoveryClick = onSetUpRecoveryClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, + onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, onRoomClick = onRoomClick, ) } @@ -133,6 +135,7 @@ private fun RoomsView( onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, + onMigrateToNativeSlidingSyncClick: () -> Unit, modifier: Modifier = Modifier, ) { if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) { @@ -147,6 +150,7 @@ private fun RoomsView( onSetUpRecoveryClick = onSetUpRecoveryClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onRoomClick = onRoomClick, + onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, modifier = modifier.fillMaxSize(), ) } @@ -159,6 +163,7 @@ private fun RoomsViewList( onSetUpRecoveryClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit, + onMigrateToNativeSlidingSyncClick: () -> Unit, modifier: Modifier = Modifier, ) { val lazyListState = rememberLazyListState() @@ -185,7 +190,7 @@ private fun RoomsViewList( item { SetUpRecoveryKeyBanner( onContinueClick = onSetUpRecoveryClick, - onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) } + onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) } ) } } @@ -193,7 +198,15 @@ private fun RoomsViewList( item { ConfirmRecoveryKeyBanner( onContinueClick = onConfirmRecoveryKeyClick, - onDismissClick = { updatedEventSink(RoomListEvents.DismissRecoveryKeyPrompt) } + onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) } + ) + } + } + SecurityBannerState.NeedsNativeSlidingSyncMigration -> { + item { + NativeSlidingSyncMigrationBanner( + onContinueClick = onMigrateToNativeSlidingSyncClick, + onDismissClick = { updatedEventSink(RoomListEvents.DismissBanner) } ) } } @@ -278,5 +291,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr onConfirmRecoveryKeyClick = {}, onRoomClick = {}, onCreateRoomClick = {}, + onMigrateToNativeSlidingSyncClick = {}, ) } diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index 6affb2bb16..ff6c626c63 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -1,5 +1,8 @@ + "Log Out & Upgrade" + "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later." + "Upgrade available" "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices." "Set up recovery" "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup." diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index 1b639d2b19..8b7294b165 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -7,6 +7,7 @@ package io.element.android.features.roomlist.impl +import androidx.compose.runtime.Composable import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test @@ -18,6 +19,8 @@ import io.element.android.features.invite.api.response.anAcceptDeclineInviteStat import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter +import io.element.android.features.logout.api.direct.DirectLogoutPresenter +import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.roomlist.impl.datasource.RoomListDataSource @@ -240,7 +243,7 @@ class RoomListPresenterTest { sessionVerificationService = FakeSessionVerificationService().apply { givenNeedsSessionVerification(false) }, - syncService = FakeSyncService(MutableStateFlow(SyncState.Running)) + syncService = FakeSyncService(MutableStateFlow(SyncState.Running)), ) val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( @@ -268,7 +271,7 @@ class RoomListPresenterTest { assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) encryptionService.emitRecoveryState(RecoveryState.DISABLED) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SetUpRecovery) - nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) + nextState.eventSink(RoomListEvents.DismissBanner) val finalState = awaitItem() assertThat(finalState.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) scope.cancel() @@ -644,6 +647,10 @@ class RoomListPresenterTest { searchPresenter: Presenter = Presenter { aRoomListSearchState() }, acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), + logoutPresenter: DirectLogoutPresenter = object : DirectLogoutPresenter { + @Composable + override fun present() = aDirectLogoutState() + }, ) = RoomListPresenter( client = client, networkMonitor = networkMonitor, @@ -671,5 +678,6 @@ class RoomListPresenterTest { acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, fullScreenIntentPermissionsPresenter = FakeFullScreenIntentPermissionsPresenter(), notificationCleaner = notificationCleaner, + logoutPresenter = logoutPresenter, ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index 1c218c9aaa..63327d0d49 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -68,7 +68,7 @@ class RoomListViewTest { val close = rule.activity.getString(CommonStrings.action_close) rule.onNodeWithContentDescription(close).performClick() - eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt) + eventsRecorder.assertSingle(RoomListEvents.DismissBanner) } @Test @@ -86,7 +86,7 @@ class RoomListViewTest { val close = rule.activity.getString(CommonStrings.action_close) rule.onNodeWithContentDescription(close).performClick() - eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt) + eventsRecorder.assertSingle(RoomListEvents.DismissBanner) } @Test @@ -232,6 +232,21 @@ class RoomListViewTest { listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)), ) } + + @Test + fun `clicking on logout and migrate calls the migration clicked callback`() { + val state = aRoomListState( + contentState = aRoomsContentState(securityBannerState = SecurityBannerState.NeedsNativeSlidingSyncMigration), + eventSink = {}, + ) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = state, + onMigrateToNativeSlidingSyncClick = callback, + ) + rule.clickOn(R.string.banner_migrate_to_native_sliding_sync_action) + } + } } private fun AndroidComposeTestRule.setRoomListView( @@ -244,6 +259,7 @@ private fun AndroidComposeTestRule.setRoomL onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(), + onMigrateToNativeSlidingSyncClick: () -> Unit = EnsureNeverCalled() ) { setContent { RoomListView( @@ -256,6 +272,7 @@ private fun AndroidComposeTestRule.setRoomL onRoomSettingsClick = onRoomSettingsClick, onMenuActionClick = onMenuActionClick, onRoomDirectorySearchClick = onRoomDirectorySearchClick, + onMigrateToNativeSlidingSyncClick = onMigrateToNativeSlidingSyncClick, acceptDeclineInviteView = { }, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt index b9fd8d7fe0..966ce5b0b9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt @@ -39,6 +39,7 @@ fun DialogLikeBannerMolecule( onSubmitClick: () -> Unit, onDismissClick: (() -> Unit)?, modifier: Modifier = Modifier, + actionText: String = stringResource(CommonStrings.action_continue), ) { Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { Surface( @@ -74,7 +75,7 @@ fun DialogLikeBannerMolecule( ) Spacer(modifier = Modifier.height(12.dp)) Button( - text = stringResource(CommonStrings.action_continue), + text = actionText, size = ButtonSize.Medium, modifier = Modifier.fillMaxWidth(), onClick = onSubmitClick, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 65dfa39c27..2591a5cfd3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -126,4 +126,10 @@ interface MatrixClient : Closeable { */ suspend fun getUrl(url: String): Result suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result + + /** Returns `true` if the home server supports native sliding sync. */ + suspend fun isNativeSlidingSyncSupported(): Boolean + + /** Returns `true` if the current session is using native sliding sync. */ + fun isUsingNativeSlidingSync(): Boolean } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index b7fb6d1566..86c14c2e54 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -93,6 +93,7 @@ import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.PowerLevels import org.matrix.rustcomponents.sdk.SendQueueRoomErrorListener +import org.matrix.rustcomponents.sdk.SlidingSyncVersion import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.use import timber.log.Timber @@ -528,6 +529,14 @@ class RustMatrixClient( }) }.buffer(Channel.UNLIMITED) + override suspend fun isNativeSlidingSyncSupported(): Boolean { + return client.availableSlidingSyncVersions().contains(SlidingSyncVersion.Native) + } + + override fun isUsingNativeSlidingSync(): Boolean { + return client.session().slidingSyncVersion == SlidingSyncVersion.Native + } + internal fun setDelegate(delegate: RustClientSessionDelegate) { client.setDelegate(delegate) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 040f24fb95..f4085bfb51 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -77,6 +77,8 @@ class FakeMatrixClient( private val clearCacheLambda: () -> Unit = { lambdaError() }, private val userIdServerNameLambda: () -> String = { lambdaError() }, private val getUrlLambda: (String) -> Result = { lambdaError() }, + var isNativeSlidingSyncSupportedLambda: suspend () -> Boolean = { true }, + var isUsingNativeSlidingSyncLambda: () -> Boolean = { true }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -316,4 +318,12 @@ class FakeMatrixClient( override suspend fun getUrl(url: String): Result { return getUrlLambda(url) } + + override suspend fun isNativeSlidingSyncSupported(): Boolean { + return isNativeSlidingSyncSupportedLambda() + } + + override fun isUsingNativeSlidingSync(): Boolean { + return isUsingNativeSlidingSyncLambda() + } } diff --git a/libraries/preferences/api/build.gradle.kts b/libraries/preferences/api/build.gradle.kts index a8a1566987..b9ab6630df 100644 --- a/libraries/preferences/api/build.gradle.kts +++ b/libraries/preferences/api/build.gradle.kts @@ -17,4 +17,8 @@ dependencies { implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) implementation(libs.androidx.datastore.preferences) + + testImplementation(projects.libraries.preferences.test) + testImplementation(libs.test.truth) + testImplementation(libs.coroutines.test) } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt new file mode 100644 index 0000000000..e5bcbad18e --- /dev/null +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCase.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.preferences.api.store + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class EnableNativeSlidingSyncUseCase @Inject constructor( + private val appPreferencesStore: AppPreferencesStore, + private val appCoroutineScope: CoroutineScope, +) { + operator fun invoke() { + appCoroutineScope.launch { + appPreferencesStore.setSimplifiedSlidingSyncEnabled(true) + } + } +} diff --git a/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt new file mode 100644 index 0000000000..315c8f91d6 --- /dev/null +++ b/libraries/preferences/api/src/test/kotlin/io/element/android/libraries/preferences/api/store/EnableNativeSlidingSyncUseCaseTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.preferences.api.store + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class EnableNativeSlidingSyncUseCaseTest { + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `ensure that the use case sets the simplified sliding sync enabled flag`() = runTest { + val preferencesStore = InMemoryAppPreferencesStore() + val useCase = EnableNativeSlidingSyncUseCase(preferencesStore, this) + assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse() + + useCase() + advanceUntilIdle() + + assertThat(preferencesStore.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue() + } +} diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 40a930d7e6..ae7b6d58d4 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.features.roomlist.impl) implementation(projects.features.leaveroom.impl) implementation(projects.features.login.impl) + implementation(projects.features.logout.impl) implementation(projects.features.networkmonitor.impl) implementation(projects.services.toolbox.impl) implementation(projects.libraries.featureflag.impl) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index a3516ba43f..70f2a1f8a4 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter import io.element.android.features.invite.impl.response.AcceptDeclineInviteView import io.element.android.features.leaveroom.impl.DefaultLeaveRoomPresenter +import io.element.android.features.logout.impl.direct.DefaultDirectLogoutPresenter import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor import io.element.android.features.roomlist.impl.RoomListPresenter import io.element.android.features.roomlist.impl.RoomListView @@ -144,6 +145,7 @@ class RoomListScreen( } }, notificationCleaner = FakeNotificationCleaner(), + logoutPresenter = DefaultDirectLogoutPresenter(matrixClient, encryptionService), ) @Composable @@ -172,7 +174,8 @@ class RoomListScreen( modifier = modifier, acceptDeclineInviteView = { AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {}) - } + }, + onMigrateToNativeSlidingSyncClick = {}, ) DisposableEffect(Unit) { diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png new file mode 100644 index 0000000000..d5c79327d5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45f8b8767d1a54afde878f1c853a7fd6dd5823d04db3b75a7c18c5d6fc742d8e +size 36110 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png new file mode 100644 index 0000000000..f875fdea79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6849a6c73341edc3f37fd6ef968c07e63277d18f833028386012fa2a438ade7e +size 34946 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png new file mode 100644 index 0000000000..dfcee5c2f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4ee87f2ae823b3d026b70d6b91463ba6aa3ad7e454b915c0eba3901b219169b +size 72188 diff --git a/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png new file mode 100644 index 0000000000..ff6ac65155 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.roomlist.impl.components_RoomListContentView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:863e7f66501f357728aa048560dc59fccd63bff8fc4b648296d82db119b0137d +size 71031 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index c6a256b1bd..f736825725 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -139,6 +139,7 @@ "session_verification_banner_.*", "confirm_recovery_key_banner_.*", "banner\\.set_up_recovery\\..*", + "banner\\.migrate_to_native_sliding_sync\\..*", "full_screen_intent_banner_.*", "screen_migration_.*", "screen_invites_.*" From 3294b74e39158a4329c5672ed4c44b9cc6ea7320 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Sep 2024 09:07:00 +0200 Subject: [PATCH 07/12] Pinned messages : fix timeline provider subscription --- .../pinned/PinnedEventsTimelineProvider.kt | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) 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/PinnedEventsTimelineProvider.kt index 4a1deb729b..75b639de85 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/PinnedEventsTimelineProvider.kt @@ -18,11 +18,13 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom 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 import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import javax.inject.Inject @@ -32,7 +34,8 @@ class PinnedEventsTimelineProvider @Inject constructor( private val networkMonitor: NetworkMonitor, private val featureFlagService: FeatureFlagService, ) : TimelineProvider { - private val _timelineStateFlow: MutableStateFlow> = MutableStateFlow(AsyncData.Uninitialized) + private val _timelineStateFlow: MutableStateFlow> = + MutableStateFlow(AsyncData.Uninitialized) override fun activeTimelineFlow(): StateFlow { return _timelineStateFlow @@ -44,25 +47,46 @@ class PinnedEventsTimelineProvider @Inject constructor( val timelineStateFlow = _timelineStateFlow fun launchIn(scope: CoroutineScope) { + _timelineStateFlow.subscriptionCount + .map { count -> count > 0 } + .distinctUntilChanged() + .onEach { isActive -> + if (isActive) { + onActive() + } else { + onInactive() + } + } + .launchIn(scope) + } + + private suspend fun onActive() = coroutineScope { combine( featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents), networkMonitor.connectivity - ) { - // do not use connectivity here as data can be loaded from cache, it's just to trigger retry if needed - isEnabled, _ -> + ) { isEnabled, _ -> + // do not use connectivity here as data can be loaded from cache, it's just to trigger retry if needed isEnabled } .onEach { isFeatureEnabled -> if (isFeatureEnabled) { loadTimelineIfNeeded() } else { - _timelineStateFlow.value = AsyncData.Uninitialized + resetTimeline() } } - .onCompletion { - invokeOnTimeline { close() } - } - .launchIn(scope) + .launchIn(this) + } + + private suspend fun onInactive() { + resetTimeline() + } + + private suspend fun resetTimeline() { + invokeOnTimeline { + close() + } + _timelineStateFlow.emit(AsyncData.Uninitialized) } suspend fun invokeOnTimeline(action: suspend Timeline.() -> Unit) { From 9f90761578747e83522cc1cfd947bd67cc679325 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Sep 2024 09:07:16 +0200 Subject: [PATCH 08/12] Pinned messages : allow action to continue when leaving the pinned messages list. --- .../impl/pinned/list/PinnedMessagesListPresenter.kt | 5 ++--- .../pinned/list/PinnedMessagesListPresenterTest.kt | 10 ++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) 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 f005690ef0..c25675c7f7 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 @@ -15,7 +15,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import dagger.assisted.Assisted @@ -59,6 +58,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor( private val timelineProvider: PinnedEventsTimelineProvider, private val snackbarDispatcher: SnackbarDispatcher, actionListPresenterFactory: ActionListPresenter.Factory, + private val appCoroutineScope: CoroutineScope, ) : Presenter { @AssistedFactory interface Factory { @@ -93,10 +93,9 @@ class PinnedMessagesListPresenter @AssistedInject constructor( } ) - val coroutineScope = rememberCoroutineScope() fun handleEvents(event: PinnedMessagesListEvents) { when (event) { - is PinnedMessagesListEvents.HandleAction -> coroutineScope.handleTimelineAction(event.action, event.event) + is PinnedMessagesListEvents.HandleAction -> appCoroutineScope.handleTimelineAction(event.action, event.event) } } 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 03927ec994..0478e08934 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 @@ -35,11 +35,14 @@ import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class PinnedMessagesListPresenterTest { @Test fun `present - initial state feature disabled`() = runTest { @@ -155,6 +158,7 @@ class PinnedMessagesListPresenterTest { val filledState = awaitItem() as PinnedMessagesListState.Filled val eventItem = filledState.timelineItems.first() as TimelineItem.Event filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Redact, eventItem)) + advanceUntilIdle() cancelAndIgnoreRemainingEvents() assert(redactEventLambda) .isCalledOnce() @@ -184,9 +188,11 @@ class PinnedMessagesListPresenterTest { pinnedEventsTimeline.unpinEventLambda = successUnpinEventLambda filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem)) + advanceUntilIdle() pinnedEventsTimeline.unpinEventLambda = failureUnpinEventLambda filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Unpin, eventItem)) + advanceUntilIdle() cancelAndIgnoreRemainingEvents() @@ -221,6 +227,7 @@ class PinnedMessagesListPresenterTest { val filledState = awaitItem() as PinnedMessagesListState.Filled val eventItem = filledState.timelineItems.first() as TimelineItem.Event filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewInTimeline, eventItem)) + advanceUntilIdle() cancelAndIgnoreRemainingEvents() assert(onViewInTimelineClickLambda) .isCalledOnce() @@ -249,6 +256,7 @@ class PinnedMessagesListPresenterTest { val filledState = awaitItem() as PinnedMessagesListState.Filled val eventItem = filledState.timelineItems.first() as TimelineItem.Event filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.ViewSource, eventItem)) + advanceUntilIdle() cancelAndIgnoreRemainingEvents() assert(onShowEventDebugInfoClickLambda) .isCalledOnce() @@ -277,6 +285,7 @@ class PinnedMessagesListPresenterTest { val filledState = awaitItem() as PinnedMessagesListState.Filled val eventItem = filledState.timelineItems.first() as TimelineItem.Event filledState.eventSink(PinnedMessagesListEvents.HandleAction(TimelineItemAction.Forward, eventItem)) + advanceUntilIdle() cancelAndIgnoreRemainingEvents() assert(onForwardEventClickLambda) .isCalledOnce() @@ -322,6 +331,7 @@ class PinnedMessagesListPresenterTest { timelineProvider = timelineProvider, snackbarDispatcher = SnackbarDispatcher(), actionListPresenterFactory = FakeActionListPresenter.Factory, + appCoroutineScope = this, ) } } From 412f39dd61fdb7521d83903776f6af816ec7703f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Sep 2024 09:28:00 +0200 Subject: [PATCH 09/12] Adding fastlane file for version 0.5.3 --- fastlane/metadata/android/en-US/changelogs/40005030.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40005030.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40005030.txt b/fastlane/metadata/android/en-US/changelogs/40005030.txt new file mode 100644 index 0000000000..cbdff7b20f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40005030.txt @@ -0,0 +1,2 @@ +Main changes in this version: mainly bug fixes. +Full changelog: https://github.com/element-hq/element-x-android/releases From 8bddf3ea3fd51ef3c2d20479ac815bf4e51b6edd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Sep 2024 09:29:32 +0200 Subject: [PATCH 10/12] version++ --- 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 397b9127c8..bfcfe937ff 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -47,7 +47,7 @@ private const val versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -private const val versionPatch = 3 +private const val versionPatch = 4 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch From 4690308335851f9083d7f066332cf7cd367b645c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Sep 2024 10:27:18 +0200 Subject: [PATCH 11/12] Changelog for version 0.5.3 --- CHANGES.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 9d7ea45760..2097277556 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,35 @@ +Changes in Element X v0.5.3 (2024-09-10) +======================================== + +### ✨ Features +* Add banner for optional migration to simplified sliding sync by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3429 + +### 🙌 Improvements +* Timeline : remove the encrypted history banner by @ganfra in https://github.com/element-hq/element-x-android/pull/3410 + +### 🐛 Bugfixes +* Fix new logins with Simplified SS using the proxy by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3417 +* Ensure Call is not hang up when user is asked to grant system permissions by @bmarty in https://github.com/element-hq/element-x-android/pull/3419 +* Wait for a room with joined state in `/sync` after creating it by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3421 +* [Bugfix] : fix self verification flow by @ganfra in https://github.com/element-hq/element-x-android/pull/3426 + +### 🗣 Translations +* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3425 + +### 🚧 In development 🚧 +* [Feature] Pinned messages list by @ganfra in https://github.com/element-hq/element-x-android/pull/3392 +* Pinned messages banner : adjust indicator to match design. by @ganfra in https://github.com/element-hq/element-x-android/pull/3415 + +### Dependency upgrades +* Update plugin dependencycheck to v10.0.4 by @renovate in https://github.com/element-hq/element-x-android/pull/3372 +* Update plugin detekt to v1.23.7 by @renovate in https://github.com/element-hq/element-x-android/pull/3424 + +### Others +* Delete old log files by @bmarty in https://github.com/element-hq/element-x-android/pull/3413 +* Recovery key formatting and wording iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/3409 +* Change license to AGPL by @bmarty in https://github.com/element-hq/element-x-android/pull/3422 +* Remove Wait list screen by @bmarty in https://github.com/element-hq/element-x-android/pull/3428 + Changes in Element X v0.5.2 (2024-09-05) ========================================= From 523603380ca99757d1eb86f7e5bc5f1f04e01c70 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 10 Sep 2024 10:35:06 +0200 Subject: [PATCH 12/12] Small update of the release script wording. --- tools/release/release.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tools/release/release.sh b/tools/release/release.sh index 4838d9d00e..56c6926da1 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -177,9 +177,8 @@ printf "Committing...\n" git commit -a -m 'version++' printf "\n================================================================================\n" -printf "Wait for the GitHub action https://github.com/element-hq/element-x-android/actions/workflows/release.yml?query=branch%%3Amain to build the 'main' branch.\n" -printf "Please enter the url of the github action (!!! WARNING: NOT THE URL OF THE ARTIFACT ANYMORE !!!)\n" -read -p "For instance https://github.com/element-hq/element-x-android/actions/runs/9065756777: " runUrl +printf "The GitHub action https://github.com/element-hq/element-x-android/actions/workflows/release.yml?query=branch%%3Amain should have start a new run.\n" +read -p "Please enter the url of the run, no need to wait for it to complete (example: https://github.com/element-hq/element-x-android/actions/runs/9065756777): " runUrl targetPath="./tmp/Element/${version}" @@ -270,7 +269,7 @@ printf "File app-fdroid-x86_64-release-signed.apk:\n" "${buildToolsPath}"/aapt dump badging "${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk | grep package printf "\n" -read -p "Does it look correct? Press enter when it's done." +read -p "Does it look correct? Press enter when it's done. " printf "\n================================================================================\n" printf "The APKs in ${fdroidTargetPath} have been signed!\n" @@ -363,7 +362,7 @@ read -p ". Press enter to continue. " printf "\n================================================================================\n" printf "Update the project release notes:\n\n" -read -p "Copy the content of the release note generated by GitHub to the file CHANGES.md and press enter to commit the change. \n" +read -p "Copy the content of the release note generated by GitHub to the file CHANGES.md and press enter to commit the change. " printf "\n================================================================================\n" printf "Committing...\n"