diff --git a/.github/ISSUE_TEMPLATE/task-that-belongs-to-a-story-epic.md b/.github/ISSUE_TEMPLATE/task-that-belongs-to-a-story-epic.md new file mode 100644 index 0000000000..fe49564b9d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task-that-belongs-to-a-story-epic.md @@ -0,0 +1,11 @@ +--- +name: Task that belongs to a story/epic +about: A skeleton task where the details are all contained within a story or epic + on element-meta. +title: "[Task] " +labels: T-Task +assignees: '' + +--- + +Please see and discuss the details in the meta issue. diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml index 56f5f1219b..3780c7a87d 100644 --- a/.github/workflows/generate_github_pages.yml +++ b/.github/workflows/generate_github_pages.yml @@ -32,7 +32,7 @@ jobs: mkdir -p screenshots/en cp tests/uitests/src/test/snapshots/images/* screenshots/en - name: Deploy GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./screenshots diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 8d81632f83..fe63bb677d 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml index dcb5cd3223..69833a985e 100644 --- a/.maestro/tests/account/login.yaml +++ b/.maestro/tests/account/login.yaml @@ -23,7 +23,8 @@ appId: ${MAESTRO_APP_ID} - inputText: ${MAESTRO_PASSWORD} - pressKey: Enter - tapOn: "Continue" +- runFlow: ../assertions/assertSessionVerificationDisplayed.yaml +- runFlow: ./verifySession.yaml - runFlow: ../assertions/assertAnalyticsDisplayed.yaml - tapOn: "Not now" - runFlow: ../assertions/assertHomeDisplayed.yaml -- runFlow: ./verifySession.yaml diff --git a/.maestro/tests/account/verifySession.yaml b/.maestro/tests/account/verifySession.yaml index eeb0489e3e..efc45ac3ef 100644 --- a/.maestro/tests/account/verifySession.yaml +++ b/.maestro/tests/account/verifySession.yaml @@ -1,11 +1,13 @@ appId: ${MAESTRO_APP_ID} --- -- tapOn: "Continue" - takeScreenshot: build/maestro/150-Verify - tapOn: "Enter recovery key" - tapOn: id: "verification-recovery_key" - inputText: ${MAESTRO_RECOVERY_KEY} - hideKeyboard -- tapOn: "Confirm" -- runFlow: ../assertions/assertHomeDisplayed.yaml +- tapOn: "Continue" +- extendedWaitUntil: + visible: "Device verified" + timeout: 10000 +- tapOn: "Continue" diff --git a/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml b/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml new file mode 100644 index 0000000000..6690dfddb4 --- /dev/null +++ b/.maestro/tests/assertions/assertSessionVerificationDisplayed.yaml @@ -0,0 +1,5 @@ +appId: ${MAESTRO_APP_ID} +--- +- extendedWaitUntil: + visible: "Confirm that it's you" + timeout: 20000 diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index 8b41c4d259..5d5e01de84 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -7,8 +7,4 @@ appId: ${MAESTRO_APP_ID} - tapOn: ${MAESTRO_ROOM_NAME} # Back from timeline - back -- assertVisible: "MyR" -- hideKeyboard -# Back from search -- back - runFlow: ../assertions/assertHomeDisplayed.yaml diff --git a/CHANGES.md b/CHANGES.md index ebb1bbf863..cdb2e4006e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,30 @@ +Changes in Element X v0.4.8 (2024-04-10) +======================================== + +Features ✨ +---------- + - Move session recovery to the login flow. ([#2579](https://github.com/element-hq/element-x-android/issues/2579)) + - Move session verification to the after login flow and make it mandatory. ([#2580](https://github.com/element-hq/element-x-android/issues/2580)) + - Add a notification troubleshoot screen ([#2601](https://github.com/element-hq/element-x-android/issues/2601)) + - Add action to copy permalink ([#2650](https://github.com/element-hq/element-x-android/issues/2650)) + +Bugfixes 🐛 +---------- + - Fix analytics issue around room considered as space by mistake. ([#2612](https://github.com/element-hq/element-x-android/issues/2612)) + - Fix crash observed when going back to the room list. ([#2619](https://github.com/element-hq/element-x-android/issues/2619)) + - Hide Event org.matrix.msc3401.call.member on the timeline. ([#2625](https://github.com/element-hq/element-x-android/issues/2625)) + - Fall back to name-based generated avatars when image avatars don't load. ([#2667](https://github.com/element-hq/element-x-android/issues/2667)) + +Other changes +------------- + - Improve UI for notification permission screen in onboarding. ([#2581](https://github.com/element-hq/element-x-android/issues/2581)) + - Categorise members by role in change roles screen. ([#2593](https://github.com/element-hq/element-x-android/issues/2593)) + - Make completed poll more clearly visible ([#2608](https://github.com/element-hq/element-x-android/issues/2608)) + - Show users from last visited DM as suggestion when starting a Chat or when creating a Room. ([#2634](https://github.com/element-hq/element-x-android/issues/2634)) + - Enable room moderation feature. ([#2678](https://github.com/element-hq/element-x-android/issues/2678)) + - Improve analytics opt-in screen UI. ([#2684](https://github.com/element-hq/element-x-android/issues/2684)) + + Changes in Element X v0.4.7 (2024-03-26) ======================================== diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index 99d940c5f5..ee277a40cf 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -19,6 +19,7 @@ package io.element.android.x import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -31,7 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat import com.bumble.appyx.core.integration.NodeHost import com.bumble.appyx.core.integrationpoint.NodeActivity import com.bumble.appyx.core.plugin.NodeReadyObserver @@ -60,7 +60,7 @@ class MainActivity : NodeActivity() { super.onCreate(savedInstanceState) appBindings = bindings() appBindings.lockScreenService().handleSecureFlag(this) - WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() setContent { MainContent(appBindings) } diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 853a775b45..e59799d685 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -14,16 +14,13 @@ * limitations under the License. */ plugins { - id("java-library") - id("com.android.lint") - alias(libs.plugins.kotlin.jvm) + id("io.element.android-library") alias(libs.plugins.anvil) alias(libs.plugins.ksp) } -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 +android { + namespace = "io.element.android.appconfig" } anvil { @@ -33,4 +30,5 @@ anvil { dependencies { implementation(libs.dagger) implementation(projects.libraries.di) + implementation(projects.libraries.matrix.api) } diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt index c309e01277..2ad2916550 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/TimelineConfig.kt @@ -16,6 +16,29 @@ package io.element.android.appconfig +import io.element.android.libraries.matrix.api.room.StateEventType + object TimelineConfig { const val MAX_READ_RECEIPT_TO_DISPLAY = 3 + + /** + * Event types that will be filtered out from the timeline (i.e. not displayed). + */ + val excludedEvents = listOf( + StateEventType.CALL_MEMBER, + StateEventType.ROOM_ALIASES, + StateEventType.ROOM_CANONICAL_ALIAS, + StateEventType.ROOM_GUEST_ACCESS, + StateEventType.ROOM_HISTORY_VISIBILITY, + StateEventType.ROOM_JOIN_RULES, + StateEventType.ROOM_PINNED_EVENTS, + StateEventType.ROOM_POWER_LEVELS, + StateEventType.ROOM_SERVER_ACL, + StateEventType.ROOM_TOMBSTONE, + StateEventType.SPACE_CHILD, + StateEventType.SPACE_PARENT, + StateEventType.POLICY_RULE_ROOM, + StateEventType.POLICY_RULE_SERVER, + StateEventType.POLICY_RULE_USER, + ) } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 4e436ec718..ab215f8394 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.push.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.login.impl) testImplementation(projects.tests.testutils) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt index b59c1cebdf..22300b7714 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt @@ -19,8 +19,6 @@ package io.element.android.appnav import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.room.RoomMembershipObserver -import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -34,16 +32,12 @@ import javax.inject.Inject class LoggedInEventProcessor @Inject constructor( private val snackbarDispatcher: SnackbarDispatcher, roomMembershipObserver: RoomMembershipObserver, - sessionVerificationService: SessionVerificationService, ) { private var observingJob: Job? = null private val displayLeftRoomMessage = roomMembershipObserver.updates .map { !it.isUserInRoom } - private val displayVerificationSuccessfulMessage = sessionVerificationService.verificationFlowState - .map { it == VerificationFlowState.Finished } - fun observeEvents(coroutineScope: CoroutineScope) { observingJob = coroutineScope.launch { displayLeftRoomMessage @@ -52,13 +46,6 @@ class LoggedInEventProcessor @Inject constructor( displayMessage(CommonStrings.common_current_user_left_room) } .launchIn(this) - - displayVerificationSuccessfulMessage - .filter { it } - .onEach { - displayMessage(CommonStrings.common_verification_complete) - } - .launchIn(this) } } 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 e4290d5bdf..2027f2868a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -45,6 +45,7 @@ import io.element.android.appnav.room.RoomFlowNode import io.element.android.appnav.room.RoomLoadedFlowNode 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.invitelist.api.InviteListEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint @@ -53,15 +54,16 @@ import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint +import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint -import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.waitForChildAttached import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MAIN_SPACE @@ -74,6 +76,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -87,21 +91,21 @@ class LoggedInFlowNode @AssistedInject constructor( private val preferencesEntryPoint: PreferencesEntryPoint, private val createRoomEntryPoint: CreateRoomEntryPoint, private val appNavigationStateService: AppNavigationStateService, - private val verifySessionEntryPoint: VerifySessionEntryPoint, private val secureBackupEntryPoint: SecureBackupEntryPoint, private val inviteListEntryPoint: InviteListEntryPoint, private val ftueEntryPoint: FtueEntryPoint, private val coroutineScope: CoroutineScope, private val networkMonitor: NetworkMonitor, private val notificationDrawerManager: NotificationDrawerManager, - private val ftueState: FtueState, + private val ftueService: FtueService, private val lockScreenEntryPoint: LockScreenEntryPoint, private val lockScreenStateService: LockScreenService, + private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.RoomList, + initialElement = NavTarget.Placeholder, savedStateMap = buildContext.savedStateMap, ), permanentNavModel = PermanentNavModel( @@ -119,7 +123,6 @@ class LoggedInFlowNode @AssistedInject constructor( private val loggedInFlowProcessor = LoggedInEventProcessor( snackbarDispatcher, matrixClient.roomMembershipObserver(), - matrixClient.sessionVerificationService(), ) override fun onBuilt() { @@ -131,9 +134,15 @@ class LoggedInFlowNode @AssistedInject constructor( appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE) loggedInFlowProcessor.observeEvents(coroutineScope) - if (ftueState.shouldDisplayFlow.value) { - backstack.push(NavTarget.Ftue) - } + ftueService.state + .onEach { ftueState -> + when (ftueState) { + is FtueState.Unknown -> Unit // Nothing to do + is FtueState.Incomplete -> backstack.safeRoot(NavTarget.Ftue) + is FtueState.Complete -> backstack.safeRoot(NavTarget.RoomList) + } + } + .launchIn(lifecycleScope) }, onStop = { coroutineScope.launch { @@ -189,6 +198,9 @@ class LoggedInFlowNode @AssistedInject constructor( } sealed interface NavTarget : Parcelable { + @Parcelize + data object Placeholder : NavTarget + @Parcelize data object LoggedInPermanent : NavTarget @@ -212,9 +224,6 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data object CreateRoom : NavTarget - @Parcelize - data object VerifySession : NavTarget - @Parcelize data class SecureBackup( val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root @@ -225,10 +234,14 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data object Ftue : NavTarget + + @Parcelize + data object RoomDirectorySearch : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { + NavTarget.Placeholder -> createNode(buildContext) NavTarget.LoggedInPermanent -> { createNode(buildContext) } @@ -251,10 +264,6 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.CreateRoom) } - override fun onSessionVerificationClicked() { - backstack.push(NavTarget.VerifySession) - } - override fun onSessionConfirmRecoveryKeyClicked() { backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey)) } @@ -270,6 +279,10 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onReportBugClicked() { plugins().forEach { it.onOpenBugReport() } } + + override fun onRoomDirectorySearchClicked() { + backstack.push(NavTarget.RoomDirectorySearch) + } } roomListEntryPoint .nodeBuilder(this, buildContext) @@ -299,10 +312,6 @@ class LoggedInFlowNode @AssistedInject constructor( plugins().forEach { it.onOpenBugReport() } } - override fun onVerifyClicked() { - backstack.push(NavTarget.VerifySession) - } - override fun onSecureBackupClicked() { backstack.push(NavTarget.SecureBackup()) } @@ -329,25 +338,6 @@ class LoggedInFlowNode @AssistedInject constructor( .callback(callback) .build() } - NavTarget.VerifySession -> { - val callback = object : VerifySessionEntryPoint.Callback { - override fun onEnterRecoveryKey() { - backstack.replace( - NavTarget.SecureBackup( - initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey - ) - ) - } - - override fun onDone() { - backstack.pop() - } - } - verifySessionEntryPoint - .nodeBuilder(this, buildContext) - .callback(callback) - .build() - } is NavTarget.SecureBackup -> { secureBackupEntryPoint.nodeBuilder(this, buildContext) .params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement)) @@ -372,7 +362,16 @@ class LoggedInFlowNode @AssistedInject constructor( ftueEntryPoint.nodeBuilder(this, buildContext) .callback(object : FtueEntryPoint.Callback { override fun onFtueFlowFinished() { - backstack.pop() + lifecycleScope.launch { attachRoomList() } + } + }) + .build() + } + NavTarget.RoomDirectorySearch -> { + roomDirectoryEntryPoint.nodeBuilder(this, buildContext) + .callback(object : RoomDirectoryEntryPoint.Callback { + override fun onOpenRoom(roomId: RoomId) { + coroutineScope.launch { attachRoom(roomId) } } }) .build() @@ -380,20 +379,23 @@ class LoggedInFlowNode @AssistedInject constructor( } } - suspend fun attachRoot(): Node { - return attachChild { + suspend fun attachRoomList() { + if (!canShowRoomList()) return + attachChild { backstack.singleTop(NavTarget.RoomList) } } - suspend fun attachRoom(roomId: RoomId): RoomFlowNode { - return attachChild { + suspend fun attachRoom(roomId: RoomId) { + if (!canShowRoomList()) return + attachChild { backstack.singleTop(NavTarget.RoomList) backstack.push(NavTarget.Room(roomId)) } } internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) { + if (!canShowRoomList()) return@withContext notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId) backstack.singleTop(NavTarget.RoomList) backstack.push(NavTarget.InviteList) @@ -402,13 +404,17 @@ class LoggedInFlowNode @AssistedInject constructor( } } + private fun canShowRoomList(): Boolean { + return ftueService.state.value is FtueState.Complete + } + @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { val lockScreenState by lockScreenStateService.lockState.collectAsState() + val isFtueDisplayed by ftueService.state.collectAsState() BackstackView() - val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState() - if (!isFtueDisplayed) { + if (isFtueDisplayed is FtueState.Complete) { PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) } if (lockScreenState == LockScreenLockState.Locked) { @@ -416,4 +422,10 @@ class LoggedInFlowNode @AssistedInject constructor( } } } + + @ContributesNode(AppScope::class) + class PlaceholderNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + ) : Node(buildContext, plugins = plugins) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index e4ecdd8864..d310a02b99 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -288,7 +288,7 @@ class RootFlowNode @AssistedInject constructor( .attachSession() .apply { when (deeplinkData) { - is DeeplinkData.Root -> attachRoot() + is DeeplinkData.Root -> attachRoomList() is DeeplinkData.Room -> attachRoom(deeplinkData.roomId) is DeeplinkData.InviteList -> attachInviteList(deeplinkData) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index a9c4a6ecf8..7cb1d634b8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -27,22 +27,32 @@ import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.push.api.PushService +import kotlinx.coroutines.flow.map import javax.inject.Inject class LoggedInPresenter @Inject constructor( private val matrixClient: MatrixClient, private val networkMonitor: NetworkMonitor, private val pushService: PushService, + private val sessionVerificationService: SessionVerificationService, ) : Presenter { @Composable override fun present(): LoggedInState { - LaunchedEffect(Unit) { - // Ensure pusher is registered - // TODO Manually select push provider for now - val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect - val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect - pushService.registerWith(matrixClient, pushProvider, distributor) + val isVerified by remember { + sessionVerificationService.sessionVerifiedStatus.map { it == SessionVerifiedStatus.Verified } + }.collectAsState(initial = false) + + LaunchedEffect(isVerified) { + if (isVerified) { + // Ensure pusher is registered + // TODO Manually select push provider for now + val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect + val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect + pushService.registerWith(matrixClient, pushProvider, distributor) + } } val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState() diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt index 21e412ef63..b9a537dc24 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomLoadedFlowNode.kt @@ -40,6 +40,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -61,6 +62,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val appNavigationStateService: AppNavigationStateService, private val appCoroutineScope: CoroutineScope, + private val matrixClient: MatrixClient, roomComponentFactory: RoomComponentFactory, roomMembershipObserver: RoomMembershipObserver, ) : BaseFlowNode( @@ -92,6 +94,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( Timber.v("OnCreate => ${inputs.room.roomId}") appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId) fetchRoomMembers() + trackVisitedRoom() }, onResume = { appCoroutineScope.launch { @@ -117,6 +120,10 @@ class RoomLoadedFlowNode @AssistedInject constructor( inputs() } + private fun trackVisitedRoom() = lifecycleScope.launch { + matrixClient.trackRecentlyVisitedRoom(inputs.room.roomId) + } + private fun fetchRoomMembers() = lifecycleScope.launch { inputs.room.updateMembers() } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt index ecc6a5dad0..0595ebdbea 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -33,6 +33,7 @@ import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.childNode import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import kotlinx.coroutines.CoroutineScope @@ -101,6 +102,7 @@ class RoomFlowNodeTest { roomMembershipObserver = RoomMembershipObserver(), appCoroutineScope = coroutineScope, roomComponentFactory = FakeRoomComponentFactory(), + matrixClient = FakeMatrixClient(), ) @Test diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 079ccab17a..df17053512 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -22,13 +22,11 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.networkmonitor.test.FakeNetworkMonitor -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService -import io.element.android.libraries.push.api.PushService -import io.element.android.libraries.pushproviders.api.Distributor -import io.element.android.libraries.pushproviders.api.PushProvider +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.push.test.FakePushService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.test.runTest @@ -73,20 +71,8 @@ class LoggedInPresenterTest { return LoggedInPresenter( matrixClient = FakeMatrixClient(roomListService = roomListService), networkMonitor = FakeNetworkMonitor(networkStatus), - pushService = object : PushService { - override fun notificationStyleChanged() { - } - - override fun getAvailablePushProviders(): List { - return emptyList() - } - - override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { - } - - override suspend fun testPush() { - } - } + pushService = FakePushService(), + sessionVerificationService = FakeSessionVerificationService(), ) } } diff --git a/fastlane/metadata/android/en-US/changelogs/40004080.txt b/fastlane/metadata/android/en-US/changelogs/40004080.txt new file mode 100644 index 0000000000..06f69e53ea --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40004080.txt @@ -0,0 +1,2 @@ +Main changes in this version: Enable room moderation feature. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index a51ce30f9d..08095a4e33 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -17,19 +17,14 @@ package io.element.android.features.analytics.impl import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Poll import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -45,18 +40,18 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.OnboardingBackground +import io.element.android.libraries.designsystem.components.PageTitle import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.ButtonSize -import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.TextButton -import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.persistentListOf @@ -82,6 +77,7 @@ fun AnalyticsOptInView( .fillMaxSize() .systemBarsPadding() .imePadding(), + background = { OnboardingBackground() }, header = { AnalyticsOptInHeader(state, onClickTerms) }, content = { AnalyticsOptInContent() }, footer = { @@ -103,11 +99,11 @@ private fun AnalyticsOptInHeader( Column( horizontalAlignment = Alignment.CenterHorizontally, ) { - IconTitleSubtitleMolecule( + PageTitle( modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName), - subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), - iconImageVector = Icons.Filled.Poll + subtitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), + iconStyle = BigIcon.Style.Default(CompoundIcons.Chart()) ) val text = buildAnnotatedStringWithStyledPart( R.string.screen_analytics_prompt_read_terms, @@ -136,19 +132,6 @@ private fun AnalyticsOptInHeader( } } -@Composable -private fun CheckIcon() { - Icon( - modifier = Modifier - .size(20.dp) - .background(color = MaterialTheme.colorScheme.background, shape = CircleShape) - .padding(2.dp), - imageVector = CompoundIcons.Check(), - contentDescription = null, - tint = ElementTheme.colors.textActionAccent, - ) -} - @Composable private fun AnalyticsOptInContent() { Box( @@ -162,20 +145,20 @@ private fun AnalyticsOptInContent() { items = persistentListOf( InfoListItem( message = stringResource(id = R.string.screen_analytics_prompt_data_usage), - iconComposable = { CheckIcon() }, + iconVector = CompoundIcons.CheckCircle(), ), InfoListItem( message = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing), - iconComposable = { CheckIcon() }, + iconVector = CompoundIcons.CheckCircle(), ), InfoListItem( message = stringResource(id = R.string.screen_analytics_prompt_settings), - iconComposable = { CheckIcon() }, + iconVector = CompoundIcons.CheckCircle(), ), ), - textStyle = ElementTheme.typography.fontBodyMdMedium, - iconTint = ElementTheme.colors.textPrimary, - backgroundColor = ElementTheme.colors.temporaryColorBgSpecial + textStyle = ElementTheme.typography.fontBodyLgMedium, + iconTint = ElementTheme.colors.iconSuccessPrimary, + backgroundColor = ElementTheme.colors.bgActionSecondaryHovered, ) } } diff --git a/features/call/src/main/res/values-hu/translations.xml b/features/call/src/main/res/values-hu/translations.xml index c85521c392..fee5a163bb 100644 --- a/features/call/src/main/res/values-hu/translations.xml +++ b/features/call/src/main/res/values-hu/translations.xml @@ -1,6 +1,6 @@ "Folyamatban lévő hívás" - "Koppints a híváshoz való visszatéréshez" + "Koppintson a híváshoz való visszatéréshez" "☎️ Hívás folyamatban" diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 6cd9b30741..11f87a6d9f 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -69,6 +69,8 @@ dependencies { testImplementation(projects.libraries.usersearch.test) testImplementation(projects.features.createroom.test) testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) ksp(libs.showkase.processor) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt index 4fb1149a01..63e2417113 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.userlist.SelectionMode import io.element.android.features.createroom.impl.userlist.UserListState +import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList import io.element.android.features.createroom.impl.userlist.aUserListState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.ui.components.aMatrixUserList @@ -29,13 +30,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider get() = sequenceOf( aUserListState(), - aUserListState().copy( + aUserListState( searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()), selectedUsers = aMatrixUserList().toImmutableList(), isSearchActive = false, selectionMode = SelectionMode.Multiple, ), - aUserListState().copy( + aUserListState( searchResults = SearchBarResultState.Results( aMatrixUserList() .mapIndexed { index, matrixUser -> @@ -46,6 +47,9 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider - Column( + UserListView( modifier = Modifier .fillMaxSize() .padding(padding) .consumeWindowInsets(padding), - ) { - UserListView( - modifier = Modifier - .fillMaxWidth(), - state = state, - showBackButton = false, - onUserSelected = { }, - onUserDeselected = {}, - ) - } + state = state, + showBackButton = false, + onUserSelected = {}, + onUserDeselected = {}, + ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt index 3cc009989a..0e1c448015 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/UserListView.kt @@ -19,17 +19,27 @@ package io.element.android.features.createroom.impl.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.createroom.impl.userlist.UserListEvents import io.element.android.features.createroom.impl.userlist.UserListState import io.element.android.features.createroom.impl.userlist.UserListStateProvider +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.ui.components.CheckableUserRow +import io.element.android.libraries.matrix.ui.components.CheckableUserRowData import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun UserListView( @@ -74,6 +84,43 @@ fun UserListView( }, ) } + if (!state.isSearchActive && state.recentDirectRooms.isNotEmpty()) { + LazyColumn { + item { + ListSectionHeader( + title = stringResource(id = CommonStrings.common_suggestions), + hasDivider = false, + ) + } + state.recentDirectRooms.forEachIndexed { index, recentDirectRoom -> + item { + val isSelected = state.selectedUsers.any { + recentDirectRoom.matrixUser.userId == it.userId + } + CheckableUserRow( + checked = isSelected, + onCheckedChange = { + if (isSelected) { + state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser)) + onUserDeselected(recentDirectRoom.matrixUser) + } else { + state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser)) + onUserSelected(recentDirectRoom.matrixUser) + } + }, + data = CheckableUserRowData.Resolved( + avatarData = recentDirectRoom.matrixUser.getAvatarData(AvatarSize.UserListItem), + name = recentDirectRoom.matrixUser.getBestName(), + subtext = recentDirectRoom.matrixUser.userId.value, + ), + ) + if (index < state.recentDirectRooms.lastIndex) { + HorizontalDivider() + } + } + } + } + } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt index 723c650793..0638d8abbb 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootStateProvider.kt @@ -17,9 +17,12 @@ package io.element.android.features.createroom.impl.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.createroom.impl.userlist.UserListState +import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList import io.element.android.features.createroom.impl.userlist.aUserListState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.persistentListOf @@ -28,7 +31,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider get() = sequenceOf( aCreateRoomRootState(), - aCreateRoomRootState().copy( + aCreateRoomRootState( startDmAction = AsyncAction.Loading, userListState = aMatrixUser().let { aUserListState().copy( @@ -39,7 +42,7 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, + eventSink: (CreateRoomRootEvents) -> Unit = {}, +) = CreateRoomRootState( + applicationName = applicationName, + userListState = userListState, + startDmAction = startDmAction, + eventSink = eventSink, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 4f874c57dd..33707896fa 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -46,11 +47,14 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.ListSectionHeader import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf @Composable fun CreateRoomRootView( @@ -77,7 +81,11 @@ fun CreateRoomRootView( ) { UserListView( modifier = Modifier.fillMaxWidth(), - state = state.userListState, + // Do not render suggestions in this case, the suggestion will be rendered + // by CreateRoomActionButtonsList + state = state.userListState.copy( + recentDirectRooms = persistentListOf(), + ), onUserSelected = { state.eventSink(CreateRoomRootEvents.StartDM(it)) }, @@ -89,6 +97,7 @@ fun CreateRoomRootView( state = state, onNewRoomClicked = onNewRoomClicked, onInvitePeopleClicked = onInviteFriendsClicked, + onDmClicked = onOpenDM, ) } } @@ -106,7 +115,7 @@ fun CreateRoomRootView( onRetry = { state.userListState.selectedUsers.firstOrNull() ?.let { state.eventSink(CreateRoomRootEvents.StartDM(it)) } - // Cancel start DM if there is no more selected user (should not happen) + // Cancel start DM if there is no more selected user (should not happen) ?: state.eventSink(CreateRoomRootEvents.CancelStartDM) }, onErrorDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, @@ -139,18 +148,43 @@ private fun CreateRoomActionButtonsList( state: CreateRoomRootState, onNewRoomClicked: () -> Unit, onInvitePeopleClicked: () -> Unit, + onDmClicked: (RoomId) -> Unit, ) { - Column { - CreateRoomActionButton( - iconRes = CompoundDrawables.ic_compound_plus, - text = stringResource(id = R.string.screen_create_room_action_create_room), - onClick = onNewRoomClicked, - ) - CreateRoomActionButton( - iconRes = CompoundDrawables.ic_compound_share_android, - text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName), - onClick = onInvitePeopleClicked, - ) + LazyColumn { + item { + CreateRoomActionButton( + iconRes = CompoundDrawables.ic_compound_plus, + text = stringResource(id = R.string.screen_create_room_action_create_room), + onClick = onNewRoomClicked, + ) + } + item { + CreateRoomActionButton( + iconRes = CompoundDrawables.ic_compound_share_android, + text = stringResource(id = CommonStrings.action_invite_friends_to_app, state.applicationName), + onClick = onInvitePeopleClicked, + ) + } + if (state.userListState.recentDirectRooms.isNotEmpty()) { + item { + ListSectionHeader( + title = stringResource(id = CommonStrings.common_suggestions), + hasDivider = false, + ) + } + state.userListState.recentDirectRooms.forEach { recentDirectRoom -> + item { + MatrixUserRow( + modifier = Modifier.clickable( + onClick = { + onDmClicked(recentDirectRoom.roomId) + } + ), + matrixUser = recentDirectRoom.matrixUser, + ) + } + } + } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt index a210a6debd..31daf62513 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt @@ -30,6 +30,9 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom +import io.element.android.libraries.matrix.api.room.recent.getRecentDirectRooms import io.element.android.libraries.usersearch.api.UserRepository import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.ImmutableList @@ -41,6 +44,7 @@ class DefaultUserListPresenter @AssistedInject constructor( @Assisted val args: UserListPresenterArgs, @Assisted val userRepository: UserRepository, @Assisted val userListDataStore: UserListDataStore, + private val matrixClient: MatrixClient, ) : UserListPresenter { @AssistedFactory @ContributesBinding(SessionScope::class) @@ -54,6 +58,10 @@ class DefaultUserListPresenter @AssistedInject constructor( @Composable override fun present(): UserListState { + var recentDirectRooms by remember { mutableStateOf(emptyList()) } + LaunchedEffect(Unit) { + recentDirectRooms = matrixClient.getRecentDirectRooms() + } var isSearchActive by rememberSaveable { mutableStateOf(false) } val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList()) var searchQuery by rememberSaveable { mutableStateOf("") } @@ -82,6 +90,7 @@ class DefaultUserListPresenter @AssistedInject constructor( isSearchActive = isSearchActive, showSearchLoader = showSearchLoader, selectionMode = args.selectionMode, + recentDirectRooms = recentDirectRooms.toImmutableList(), eventSink = { event -> when (event) { is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt index a27191881e..b7b61fbe52 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt @@ -17,6 +17,7 @@ package io.element.android.features.createroom.impl.userlist import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.ImmutableList @@ -28,6 +29,7 @@ data class UserListState( val selectedUsers: ImmutableList, val isSearchActive: Boolean, val selectionMode: SelectionMode, + val recentDirectRooms: ImmutableList, val eventSink: (UserListEvents) -> Unit, ) { val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt index 193fa7e71f..fc46ae1953 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt @@ -18,54 +18,82 @@ package io.element.android.features.createroom.impl.userlist import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.recent.RecentDirectRoom +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.usersearch.api.UserSearchResult -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList open class UserListStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aUserListState(), - aUserListState().copy( + aUserListState( isSearchActive = false, selectedUsers = aListOfSelectedUsers(), selectionMode = SelectionMode.Multiple, ), - aUserListState().copy(isSearchActive = true), - aUserListState().copy(isSearchActive = true, searchQuery = "someone"), - aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), - aUserListState().copy( + aUserListState(isSearchActive = true), + aUserListState(isSearchActive = true, searchQuery = "someone"), + aUserListState(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Multiple), + aUserListState( isSearchActive = true, searchQuery = "@someone:matrix.org", selectedUsers = aMatrixUserList().toImmutableList(), searchResults = SearchBarResultState.Results(aListOfUserSearchResults()), ), - aUserListState().copy( + aUserListState( isSearchActive = true, searchQuery = "@someone:matrix.org", selectionMode = SelectionMode.Multiple, selectedUsers = aMatrixUserList().toImmutableList(), searchResults = SearchBarResultState.Results(aListOfUserSearchResults()), ), - aUserListState().copy( + aUserListState( isSearchActive = true, searchQuery = "something-with-no-results", searchResults = SearchBarResultState.NoResultsFound() ), - aUserListState().copy(isSearchActive = true, searchQuery = "someone", selectionMode = SelectionMode.Single), + aUserListState( + isSearchActive = true, + searchQuery = "someone", + selectionMode = SelectionMode.Single, + ), + aUserListState( + recentDirectRooms = aRecentDirectRoomList(), + ), ) } -fun aUserListState() = UserListState( - isSearchActive = false, - searchQuery = "", - searchResults = SearchBarResultState.Initial(), - selectedUsers = persistentListOf(), - selectionMode = SelectionMode.Single, - showSearchLoader = false, - eventSink = {} +fun aUserListState( + searchQuery: String = "", + isSearchActive: Boolean = false, + searchResults: SearchBarResultState> = SearchBarResultState.Initial(), + selectedUsers: List = emptyList(), + showSearchLoader: Boolean = false, + selectionMode: SelectionMode = SelectionMode.Single, + recentDirectRooms: List = emptyList(), + eventSink: (UserListEvents) -> Unit = {}, +) = UserListState( + searchQuery = searchQuery, + isSearchActive = isSearchActive, + searchResults = searchResults, + selectedUsers = selectedUsers.toImmutableList(), + showSearchLoader = showSearchLoader, + selectionMode = selectionMode, + recentDirectRooms = recentDirectRooms.toImmutableList(), + eventSink = eventSink ) fun aListOfSelectedUsers() = aMatrixUserList().take(6).toImmutableList() fun aListOfUserSearchResults() = aMatrixUserList().take(6).map { UserSearchResult(it) }.toImmutableList() + +fun aRecentDirectRoomList( + count: Int = 5 +): List = aMatrixUserList() + .take(count) + .map { + RecentDirectRoom(RoomId("!aRoom:id"), it) + } diff --git a/features/createroom/impl/src/main/res/values-be/translations.xml b/features/createroom/impl/src/main/res/values-be/translations.xml index 066f3332de..3c81815d7d 100644 --- a/features/createroom/impl/src/main/res/values-be/translations.xml +++ b/features/createroom/impl/src/main/res/values-be/translations.xml @@ -7,7 +7,7 @@ "Паведамленні ў гэтым пакоі зашыфраваны. Гэта шыфраванне нельга адключыць." "Прыватны пакой (толькі па запрашэнні)" "Паведамленні не зашыфраваны, і кожны можа іх прачытаць. Вы можаце ўключыць шыфраванне пазней." - "Адкрыты пакой (для ўсіх)" + "Публічны пакой (для ўсіх)" "Назва пакоя" "Стварыце пакой" "Тэма (неабавязкова)" diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt new file mode 100644 index 0000000000..36741347e5 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleViewTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.addpeople + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.createroom.impl.userlist.UserListEvents +import io.element.android.features.createroom.impl.userlist.UserListState +import io.element.android.features.createroom.impl.userlist.aUserListState +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AddPeopleViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { + rule.setAddPeopleView( + aUserListState( + eventSink = eventsRecorder, + ), + onBackPressed = it + ) + rule.pressBack() + } + eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery("")) + } + + @Test + fun `clicking on back during search emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setAddPeopleView( + aUserListState( + isSearchActive = true, + eventSink = eventsRecorder, + ), + ) + rule.pressBack() + eventsRecorder.assertSingle(UserListEvents.OnSearchActiveChanged(false)) + } + + @Test + fun `clicking on skip invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { + rule.setAddPeopleView( + aUserListState( + eventSink = eventsRecorder, + ), + onNextPressed = it + ) + rule.clickOn(CommonStrings.action_skip) + } + eventsRecorder.assertSingle(UserListEvents.UpdateSearchQuery("")) + } +} + +private fun AndroidComposeTestRule.setAddPeopleView( + state: UserListState, + onBackPressed: () -> Unit = EnsureNeverCalled(), + onNextPressed: () -> Unit = EnsureNeverCalled(), +) { + setContent { + AddPeopleView( + state = state, + onBackPressed = onBackPressed, + onNextPressed = onNextPressed, + ) + } +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootViewTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootViewTest.kt new file mode 100644 index 0000000000..dcb2e02347 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootViewTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.root + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.createroom.impl.R +import io.element.android.features.createroom.impl.userlist.aRecentDirectRoomList +import io.element.android.features.createroom.impl.userlist.aUserListState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class CreateRoomRootViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setCreateRoomRootView( + aCreateRoomRootState( + eventSink = eventsRecorder, + ), + onClosePressed = it + ) + rule.pressBack() + } + } + + @Test + fun `clicking on New room invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setCreateRoomRootView( + aCreateRoomRootState( + eventSink = eventsRecorder, + ), + onNewRoomClicked = it + ) + rule.clickOn(R.string.screen_create_room_action_create_room) + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on Invite people invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setCreateRoomRootView( + aCreateRoomRootState( + applicationName = "test", + eventSink = eventsRecorder, + ), + onInviteFriendsClicked = it + ) + val text = rule.activity.getString(CommonStrings.action_invite_friends_to_app, "test") + rule.onNodeWithText(text).performClick() + } + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on a user suggestion invokes the expected callback`() { + val recentDirectRoomList = aRecentDirectRoomList() + val firstRoom = recentDirectRoomList[0] + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnceWithParam(firstRoom.roomId) { + rule.setCreateRoomRootView( + aCreateRoomRootState( + userListState = aUserListState( + recentDirectRooms = recentDirectRoomList + ), + eventSink = eventsRecorder, + ), + onOpenDM = it + ) + rule.onNodeWithText(firstRoom.matrixUser.getBestName()).performClick() + } + } +} + +private fun AndroidComposeTestRule.setCreateRoomRootView( + state: CreateRoomRootState, + onClosePressed: () -> Unit = EnsureNeverCalled(), + onNewRoomClicked: () -> Unit = EnsureNeverCalled(), + onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onInviteFriendsClicked: () -> Unit = EnsureNeverCalled(), +) { + setContent { + CreateRoomRootView( + state = state, + onClosePressed = onClosePressed, + onNewRoomClicked = onNewRoomClicked, + onOpenDM = onOpenDM, + onInviteFriendsClicked = onInviteFriendsClicked, + ) + } +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt index 579bd175f5..c4d6d9ab00 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt @@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.usersearch.api.UserSearchResult @@ -45,6 +46,7 @@ class DefaultUserListPresenterTests { UserListPresenterArgs(selectionMode = SelectionMode.Single), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -66,6 +68,7 @@ class DefaultUserListPresenterTests { UserListPresenterArgs(selectionMode = SelectionMode.Multiple), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -87,6 +90,7 @@ class DefaultUserListPresenterTests { UserListPresenterArgs(selectionMode = SelectionMode.Single), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -123,6 +127,7 @@ class DefaultUserListPresenterTests { ), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -175,6 +180,7 @@ class DefaultUserListPresenterTests { ), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -200,6 +206,7 @@ class DefaultUserListPresenterTests { UserListPresenterArgs(selectionMode = SelectionMode.Single), userRepository, UserListDataStore(), + FakeMatrixClient(), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt new file mode 100644 index 0000000000..77dd258b22 --- /dev/null +++ b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueService.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.api.state + +import kotlinx.coroutines.flow.StateFlow + +/** + * Service to manage the First Time User Experience state (aka Onboarding). + */ +interface FtueService { + /** The current state of the FTUE. */ + val state: StateFlow + + /** Reset the FTUE state. */ + suspend fun reset() +} + +/** The state of the FTUE. */ +sealed interface FtueState { + /** The FTUE state is unknown, nothing to do for now. */ + data object Unknown : FtueState + + /** The FTUE state is incomplete. The FTUE flow should be displayed. */ + data object Incomplete : FtueState + + /** The FTUE state is complete. The FTUE flow should not be displayed anymore. */ + data object Complete : FtueState +} diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index 1719aecbe1..06251cfe5d 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -42,6 +42,8 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) implementation(projects.features.analytics.api) + implementation(projects.features.securebackup.api) + implementation(projects.features.verifysession.api) implementation(projects.services.analytics.api) implementation(projects.features.lockscreen.api) implementation(projects.libraries.permissions.api) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index 652b6dddd4..cb77100d94 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -34,7 +34,8 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode -import io.element.android.features.ftue.impl.state.DefaultFtueState +import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode +import io.element.android.features.ftue.impl.state.DefaultFtueService import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.libraries.architecture.BackstackView @@ -55,7 +56,7 @@ import kotlinx.parcelize.Parcelize class FtueFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val ftueState: DefaultFtueState, + private val ftueState: DefaultFtueService, private val analyticsEntryPoint: AnalyticsEntryPoint, private val analyticsService: AnalyticsService, private val lockScreenEntryPoint: LockScreenEntryPoint, @@ -72,6 +73,9 @@ class FtueFlowNode @AssistedInject constructor( @Parcelize data object Placeholder : NavTarget + @Parcelize + data object SessionVerification : NavTarget + @Parcelize data object NotificationsOptIn : NavTarget @@ -106,6 +110,14 @@ class FtueFlowNode @AssistedInject constructor( NavTarget.Placeholder -> { createNode(buildContext) } + NavTarget.SessionVerification -> { + val callback = object : FtueSessionVerificationFlowNode.Callback { + override fun onDone() { + lifecycleScope.launch { moveToNextStep() } + } + } + createNode(buildContext, listOf(callback)) + } NavTarget.NotificationsOptIn -> { val callback = object : NotificationsOptInNode.Callback { override fun onNotificationsOptInFinished() { @@ -133,6 +145,9 @@ class FtueFlowNode @AssistedInject constructor( private fun moveToNextStep() { when (ftueState.getNextStep()) { + FtueStep.SessionVerification -> { + backstack.newRoot(NavTarget.SessionVerification) + } FtueStep.NotificationsOptIn -> { backstack.newRoot(NavTarget.NotificationsOptIn) } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt index 924824a2ec..b9f902202b 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -40,8 +40,10 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.ftue.impl.R import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.OnboardingBackground +import io.element.android.libraries.designsystem.components.PageTitle import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize @@ -62,8 +64,9 @@ fun NotificationsOptInView( HeaderFooterPage( modifier = modifier - .systemBarsPadding() + .statusBarsPadding() .fillMaxSize(), + background = { OnboardingBackground() }, header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, footer = { NotificationsOptInFooter(state) }, ) { @@ -75,11 +78,11 @@ fun NotificationsOptInView( private fun NotificationsOptInHeader( modifier: Modifier = Modifier, ) { - IconTitleSubtitleMolecule( + PageTitle( modifier = modifier, title = stringResource(R.string.screen_notification_optin_title), - subTitle = stringResource(R.string.screen_notification_optin_subtitle), - iconImageVector = CompoundIcons.NotificationsSolid(), + subtitle = stringResource(R.string.screen_notification_optin_subtitle), + iconStyle = BigIcon.Style.Default(CompoundIcons.NotificationsSolid()), ) } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt new file mode 100644 index 0000000000..f59c76199a --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.ftue.impl.sessionverification + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +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 com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.newRoot +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.securebackup.api.SecureBackupEntryPoint +import io.element.android.features.verifysession.api.VerifySessionEntryPoint +import io.element.android.libraries.architecture.BackstackView +import io.element.android.libraries.architecture.BaseFlowNode +import io.element.android.libraries.di.SessionScope +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class FtueSessionVerificationFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val verifySessionEntryPoint: VerifySessionEntryPoint, + private val secureBackupEntryPoint: SecureBackupEntryPoint, +) : BaseFlowNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data object EnterRecoveryKey : NavTarget + + @Parcelize + data object CreateNewRecoveryKey : NavTarget + } + + interface Callback : Plugin { + fun onDone() + } + + private val secureBackupEntryPointCallback = object : SecureBackupEntryPoint.Callback { + override fun onCreateNewRecoveryKey() { + backstack.push(NavTarget.CreateNewRecoveryKey) + } + + override fun onDone() { + lifecycleScope.launch { + // Move to the completed state view in the verification flow + backstack.newRoot(NavTarget.Root) + } + } + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.Root -> { + verifySessionEntryPoint.nodeBuilder(this, buildContext) + .callback(object : VerifySessionEntryPoint.Callback { + override fun onEnterRecoveryKey() { + backstack.push(NavTarget.EnterRecoveryKey) + } + + override fun onCreateNewRecoveryKey() { + backstack.push(NavTarget.CreateNewRecoveryKey) + } + + override fun onDone() { + plugins().forEach { it.onDone() } + } + }) + .build() + } + is NavTarget.EnterRecoveryKey -> { + secureBackupEntryPoint.nodeBuilder(this, buildContext) + .params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey)) + .callback(secureBackupEntryPointCallback) + .build() + } + is NavTarget.CreateNewRecoveryKey -> { + secureBackupEntryPoint.nodeBuilder(this, buildContext) + .params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey)) + .callback(secureBackupEntryPointCallback) + .build() + } + } + } + + @Composable + override fun View(modifier: Modifier) { + BackstackView() + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt similarity index 79% rename from features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt rename to features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt index f1a1c84545..87b7c3a7ee 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt @@ -20,9 +20,11 @@ import android.Manifest import android.os.Build import androidx.annotation.VisibleForTesting import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider @@ -35,14 +37,15 @@ import kotlinx.coroutines.runBlocking import javax.inject.Inject @ContributesBinding(SessionScope::class) -class DefaultFtueState @Inject constructor( +class DefaultFtueService @Inject constructor( private val sdkVersionProvider: BuildVersionSdkIntProvider, coroutineScope: CoroutineScope, private val analyticsService: AnalyticsService, private val permissionStateProvider: PermissionStateProvider, private val lockScreenService: LockScreenService, -) : FtueState { - override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete()) + private val sessionVerificationService: SessionVerificationService, +) : FtueService { + override val state = MutableStateFlow(FtueState.Unknown) override suspend fun reset() { analyticsService.reset() @@ -52,6 +55,10 @@ class DefaultFtueState @Inject constructor( } init { + sessionVerificationService.needsVerificationFlow + .onEach { updateState() } + .launchIn(coroutineScope) + analyticsService.didAskUserConsent() .onEach { updateState() } .launchIn(coroutineScope) @@ -59,7 +66,12 @@ class DefaultFtueState @Inject constructor( fun getNextStep(currentStep: FtueStep? = null): FtueStep? = when (currentStep) { - null -> if (shouldAskNotificationPermissions()) { + null -> if (isSessionNotVerified()) { + FtueStep.SessionVerification + } else { + getNextStep(FtueStep.SessionVerification) + } + FtueStep.SessionVerification -> if (shouldAskNotificationPermissions()) { FtueStep.NotificationsOptIn } else { getNextStep(FtueStep.NotificationsOptIn) @@ -79,12 +91,17 @@ class DefaultFtueState @Inject constructor( private fun isAnyStepIncomplete(): Boolean { return listOf( + { isSessionNotVerified() }, { shouldAskNotificationPermissions() }, { needsAnalyticsOptIn() }, { shouldDisplayLockscreenSetup() }, ).any { it() } } + private fun isSessionNotVerified(): Boolean { + return sessionVerificationService.needsVerificationFlow.value + } + private fun needsAnalyticsOptIn(): Boolean { // We need this function to not be suspend, so we need to load the value through runBlocking return runBlocking { analyticsService.didAskUserConsent().first().not() } @@ -109,11 +126,15 @@ class DefaultFtueState @Inject constructor( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun updateState() { - shouldDisplayFlow.value = isAnyStepIncomplete() + state.value = when { + isAnyStepIncomplete() -> FtueState.Incomplete + else -> FtueState.Complete + } } } sealed interface FtueStep { + data object SessionVerification : FtueStep data object NotificationsOptIn : FtueStep data object AnalyticsOptIn : FtueStep data object LockscreenSetup : FtueStep diff --git a/features/ftue/impl/src/main/res/values-be/translations.xml b/features/ftue/impl/src/main/res/values-be/translations.xml index 413bad9132..0b46417dd3 100644 --- a/features/ftue/impl/src/main/res/values-be/translations.xml +++ b/features/ftue/impl/src/main/res/values-be/translations.xml @@ -2,6 +2,13 @@ "Вы можаце змяніць налады пазней." "Дазвольце апавяшчэнні і ніколі не прапускайце іх" + "Адкрыйце Element на настольнай прыладзе" + "Націсніце на свой аватар" + "Выберыце %1$s" + "“Звязаць новую прыладу”" + "Выберыце %1$s" + "“Паказаць QR-код”" + "Адкрыйце Element на іншай прыладзе, каб атрымаць QR-код" "Званкі, апытанні, пошук і многае іншае будзе дададзена пазней у гэтым годзе." "Гісторыя паведамленняў для зашыфраваных пакояў пакуль недаступна." "Мы будзем рады пачуць вашае меркаванне, паведаміце нам аб гэтым праз старонку налад." diff --git a/features/ftue/impl/src/main/res/values-ru/translations.xml b/features/ftue/impl/src/main/res/values-ru/translations.xml index 6ddceef57f..895c7068ff 100644 --- a/features/ftue/impl/src/main/res/values-ru/translations.xml +++ b/features/ftue/impl/src/main/res/values-ru/translations.xml @@ -2,6 +2,13 @@ "Вы можете изменить настройки позже." "Разрешите уведомления и никогда не пропустите сообщение" + "Откройте Element на настольном устройстве" + "Нажмите на свое изображение" + "Выбрать %1$s" + "\"Привязать новое устройство\"" + "Выбрать %1$s" + "\"Показать QR-код\"" + "Откройте Element на другом устройстве, чтобы получить QR-код" "Звонки, опросы, поиск и многое другое будут добавлены позже в этом году." "История сообщений для зашифрованных комнат в этом обновлении будет недоступна." "Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек." diff --git a/features/ftue/impl/src/main/res/values/localazy.xml b/features/ftue/impl/src/main/res/values/localazy.xml index 3e8c86b761..ba227878c1 100644 --- a/features/ftue/impl/src/main/res/values/localazy.xml +++ b/features/ftue/impl/src/main/res/values/localazy.xml @@ -2,6 +2,13 @@ "You can change your settings later." "Allow notifications and never miss a message" + "Open Element on a desktop device" + "Click on your avatar" + "Select %1$s" + "“Link new device”" + "Select %1$s" + "“Show QR code”" + "Open Element on another device to get the QR code" "Calls, polls, search and more will be added later this year." "Message history for encrypted rooms isn’t available yet." "We’d love to hear from you, let us know what you think via the settings page." diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt similarity index 70% rename from features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt rename to features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt index 1f0b817850..d381d945a9 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt @@ -17,11 +17,15 @@ package io.element.android.features.ftue.impl import android.os.Build +import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.ftue.impl.state.DefaultFtueState +import io.element.android.features.ftue.api.state.FtueState +import io.element.android.features.ftue.impl.state.DefaultFtueService import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.test.FakeLockScreenService +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus +import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.permissions.impl.FakePermissionStateProvider import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService @@ -32,38 +36,51 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.test.runTest import org.junit.Test -class DefaultFtueStateTests { +class DefaultFtueServiceTests { @Test - fun `given any check being false, should display flow is true`() = runTest { + fun `given any check being false and session verification state being loaded, FtueState is Incomplete`() = runTest { + val sessionVerificationService = FakeSessionVerificationService().apply { + givenVerifiedStatus(SessionVerifiedStatus.Unknown) + } val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) - val state = createState(coroutineScope) + val state = createState(coroutineScope, sessionVerificationService) - assertThat(state.shouldDisplayFlow.value).isTrue() + state.state.test { + // Verification state is unknown, we don't display the flow yet + assertThat(awaitItem()).isEqualTo(FtueState.Unknown) + + // Verification state is known, we should display the flow if any check is false + sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified) + assertThat(awaitItem()).isEqualTo(FtueState.Incomplete) + } // Cleanup coroutineScope.cancel() } @Test - fun `given all checks being true, should display flow is false`() = runTest { + fun `given all checks being true, FtueState is Complete`() = runTest { val analyticsService = FakeAnalyticsService() + val sessionVerificationService = FakeSessionVerificationService() val permissionStateProvider = FakePermissionStateProvider(permissionGranted = true) val lockScreenService = FakeLockScreenService() val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) val state = createState( coroutineScope = coroutineScope, + sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, permissionStateProvider = permissionStateProvider, lockScreenService = lockScreenService, ) + sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified) analyticsService.setDidAskUserConsent() permissionStateProvider.setPermissionGranted() lockScreenService.setIsPinSetup(true) state.updateState() - assertThat(state.shouldDisplayFlow.value).isFalse() + assertThat(state.state.value).isEqualTo(FtueState.Complete) // Cleanup coroutineScope.cancel() @@ -71,6 +88,10 @@ class DefaultFtueStateTests { @Test fun `traverse flow`() = runTest { + val sessionVerificationService = FakeSessionVerificationService().apply { + givenVerifiedStatus(SessionVerifiedStatus.NotVerified) + givenNeedsVerification(true) + } val analyticsService = FakeAnalyticsService() val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val lockScreenService = FakeLockScreenService() @@ -78,12 +99,17 @@ class DefaultFtueStateTests { val state = createState( coroutineScope = coroutineScope, + sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, permissionStateProvider = permissionStateProvider, lockScreenService = lockScreenService, ) val steps = mutableListOf() + // Session verification + steps.add(state.getNextStep(steps.lastOrNull())) + sessionVerificationService.givenNeedsVerification(false) + // Notifications opt in steps.add(state.getNextStep(steps.lastOrNull())) permissionStateProvider.setPermissionGranted() @@ -100,6 +126,7 @@ class DefaultFtueStateTests { steps.add(state.getNextStep(steps.lastOrNull())) assertThat(steps).containsExactly( + FtueStep.SessionVerification, FtueStep.NotificationsOptIn, FtueStep.LockscreenSetup, FtueStep.AnalyticsOptIn, @@ -114,17 +141,20 @@ class DefaultFtueStateTests { @Test fun `if a check for a step is true, start from the next one`() = runTest { val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + val sessionVerificationService = FakeSessionVerificationService() val analyticsService = FakeAnalyticsService() val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) val lockScreenService = FakeLockScreenService() val state = createState( coroutineScope = coroutineScope, + sessionVerificationService = sessionVerificationService, analyticsService = analyticsService, permissionStateProvider = permissionStateProvider, lockScreenService = lockScreenService, ) - // Skip first 2 steps + // Skip first 3 steps + sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified) permissionStateProvider.setPermissionGranted() lockScreenService.setIsPinSetup(true) @@ -140,16 +170,19 @@ class DefaultFtueStateTests { @Test fun `if version is older than 13 we don't display the notification opt in screen`() = runTest { val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + val sessionVerificationService = FakeSessionVerificationService() val analyticsService = FakeAnalyticsService() val lockScreenService = FakeLockScreenService() val state = createState( sdkIntVersion = Build.VERSION_CODES.M, + sessionVerificationService = sessionVerificationService, coroutineScope = coroutineScope, analyticsService = analyticsService, lockScreenService = lockScreenService, ) + sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified) lockScreenService.setIsPinSetup(true) assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) @@ -163,14 +196,16 @@ class DefaultFtueStateTests { private fun createState( coroutineScope: CoroutineScope, + sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), analyticsService: AnalyticsService = FakeAnalyticsService(), permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), lockScreenService: LockScreenService = FakeLockScreenService(), // First version where notification permission is required sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, - ) = DefaultFtueState( - sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), + ) = DefaultFtueService( coroutineScope = coroutineScope, + sessionVerificationService = sessionVerificationService, + sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), analyticsService = analyticsService, permissionStateProvider = permissionStateProvider, lockScreenService = lockScreenService, diff --git a/features/invitelist/impl/src/main/res/values-be/translations.xml b/features/invitelist/impl/src/main/res/values-be/translations.xml index 36c895dcb9..9a37d82357 100644 --- a/features/invitelist/impl/src/main/res/values-be/translations.xml +++ b/features/invitelist/impl/src/main/res/values-be/translations.xml @@ -1,8 +1,8 @@ - "Вы ўпэўненыя, што жадаеце адхіліць запрашэнне ў %1$s?" + "Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?" "Адхіліць запрашэнне" - "Вы ўпэўненыя, што жадаеце адмовіцца ад прыватных зносін з %1$s?" + "Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?" "Адхіліць чат" "Няма запрашэнняў" "%1$s (%2$s) запрасіў вас" diff --git a/features/leaveroom/api/src/main/res/values-be/translations.xml b/features/leaveroom/api/src/main/res/values-be/translations.xml index 79a0789738..c8bb068fa7 100644 --- a/features/leaveroom/api/src/main/res/values-be/translations.xml +++ b/features/leaveroom/api/src/main/res/values-be/translations.xml @@ -1,7 +1,7 @@ "Вы ўпэўнены, што хочаце пакінуць гэту размову? Гэта размова не з\'яўляецца публічнай, і вы не зможаце далучыцца зноў без запрашэння." - "Вы ўпэўнены, што жадаеце пакінуць гэты пакой? Вы тут адзіны карыстальнік. Калі вы выйдзеце, ніхто не зможа далучыцца ў будучыні, у тым ліку і вы." - "Вы ўпэўнены, што жадаеце пакінуць гэты пакой? Гэты пакой не агульнадаступны, і вы не зможаце далучыцца да яго зноў без запрашэння." - "Вы ўпэўнены, што жадаеце пакінуць пакой?" + "Вы ўпэўнены, што хочаце пакінуць гэты пакой? Вы тут адзіны карыстальнік. Калі вы выйдзеце, ніхто не зможа далучыцца ў будучыні, у тым ліку і вы." + "Вы ўпэўнены, што жхочаце пакінуць гэты пакой? Гэты пакой не агульнадаступны, і вы не зможаце далучыцца да яго зноў без запрашэння." + "Вы ўпэўнены, што хочаце пакінуць пакой?" diff --git a/features/leaveroom/api/src/main/res/values-hu/translations.xml b/features/leaveroom/api/src/main/res/values-hu/translations.xml index c75cddfd05..8b5136f495 100644 --- a/features/leaveroom/api/src/main/res/values-hu/translations.xml +++ b/features/leaveroom/api/src/main/res/values-hu/translations.xml @@ -2,6 +2,6 @@ "Biztos, hogy elhagyja ezt a beszélgetést? Ez a beszélgetés nem nyilvános, és meghívás nélkül nem fog tudni visszacsatlakozni." "Biztos, hogy elhagyja ezt a szobát? Ön az egyedüli ember itt. Ha kilép, akkor senki sem fog tudni csatlakozni a jövőben, Önt is beleértve." - "Biztos, hogy elhagyod ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fogsz tudni újra belépni." - "Biztos, hogy elhagyod a szobát?" + "Biztos, hogy elhagyja ezt a szobát? Ez a szoba nem nyilvános, és meghívó nélkül nem fog tudni újra belépni." + "Biztos, hogy elhagyja a szobát?" diff --git a/features/lockscreen/impl/src/main/res/values-be/translations.xml b/features/lockscreen/impl/src/main/res/values-be/translations.xml index 7a7d4acf7e..2c5bd554b0 100644 --- a/features/lockscreen/impl/src/main/res/values-be/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-be/translations.xml @@ -7,7 +7,7 @@ "Змяніць PIN-код" "Дазволіць біяметрычную разблакіроўку" "Выдаліць PIN-код" - "Вы ўпэўнены, што жадаеце выдаліць PIN-код?" + "Вы ўпэўнены, што хочаце выдаліць PIN-код?" "Выдаліць PIN-код?" "Дазволіць %1$s" "Я хацеў бы выкарыстоўваць PIN-код" @@ -25,12 +25,12 @@ "Вы выходзіце з сістэмы" "У вас %1$d спроба разблакіроўкі" - "У вас %1$d спроб разблакіроўкі" + "У вас %1$d спробы разблакіроўкі" "У вас %1$d спроб разблакіроўкі" "Няправільны PIN-код. У вас застаўся %1$d шанец" - "Няправільны PIN-код. У вас застаўася %1$d шанцаў" + "Няправільны PIN-код. У вас застаўася %1$d шанца" "Няправільны PIN-код. У вас застаўася %1$d шанцаў" "Выкарыстоўваць біяметрыю" diff --git a/features/lockscreen/impl/src/main/res/values-sv/translations.xml b/features/lockscreen/impl/src/main/res/values-sv/translations.xml index b89f26bae3..c0ffcd9cdf 100644 --- a/features/lockscreen/impl/src/main/res/values-sv/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sv/translations.xml @@ -1,4 +1,8 @@ + "Byt PIN-kod" + "Tillåt biometrisk upplåsning" + "Ta bort PIN-kod" + "Ta bort PIN-koden?" "Loggar ut …" diff --git a/features/login/impl/src/main/res/values-hu/translations.xml b/features/login/impl/src/main/res/values-hu/translations.xml index 86cd1e073e..d469e76630 100644 --- a/features/login/impl/src/main/res/values-hu/translations.xml +++ b/features/login/impl/src/main/res/values-hu/translations.xml @@ -8,7 +8,7 @@ "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez." "Hamarosan bejelentkezik ide: %s" "Itt lesznek a beszélgetései – ahogyan egy e-mail-szolgáltatást is használna a levelei kezeléséhez." - "Hamarosan létrehozol egy fiókot itt: %s" + "Hamarosan létrehoz egy fiókot itt: %s" "A Matrix.org egy nagy, ingyenes kiszolgáló a nyilvános Matrix-hálózaton, a biztonságos, decentralizált kommunikáció érdekében, amelyet a Matrix.org Alapítvány üzemeltet." "Egyéb" "Másik fiókszolgáltató, például a saját privát kiszolgáló vagy egy munkahelyi fiók használata." diff --git a/features/logout/impl/src/main/res/values-be/translations.xml b/features/logout/impl/src/main/res/values-be/translations.xml index 1398ae20d7..62425e93f6 100644 --- a/features/logout/impl/src/main/res/values-be/translations.xml +++ b/features/logout/impl/src/main/res/values-be/translations.xml @@ -1,6 +1,6 @@ - "Вы ўпэўнены, што жадаеце выйсці?" + "Вы ўпэўнены, што хочаце выйсці?" "Выйсці" "Выйсці" "Выхад…" diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index c81cbebaae..aca840236e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -16,9 +16,12 @@ package io.element.android.features.messages.impl +import android.content.Context +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node @@ -31,11 +34,14 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.mediaplayer.api.MediaPlayer @@ -52,6 +58,7 @@ class MessagesNode @AssistedInject constructor( presenterFactory: MessagesPresenter.Factory, private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val mediaPlayer: MediaPlayer, + private val permalinkParser: PermalinkParser, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create(this) private val callback = plugins().firstOrNull() @@ -96,6 +103,25 @@ class MessagesNode @AssistedInject constructor( private fun onUserDataClicked(userId: UserId) { callback?.onUserDataClicked(userId) } + + private fun onLinkClicked( + context: Context, + url: String, + ) { + when (val permalink = permalinkParser.parse(Uri.parse(url))) { + is PermalinkData.UserLink -> { + callback?.onUserDataClicked(UserId(permalink.userId)) + } + is PermalinkData.RoomLink -> { + // TODO Implement room link handling + } + is PermalinkData.FallbackLink, + is PermalinkData.RoomEmailInviteLink -> { + context.openUrlInExternalApp(url) + } + } + } + override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) { callback?.onShowEventDebugInfoClicked(eventId, debugInfo) } @@ -126,6 +152,7 @@ class MessagesNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { + val context = LocalContext.current CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { @@ -137,6 +164,7 @@ class MessagesNode @AssistedInject constructor( onEventClicked = this::onEventClicked, onPreviewAttachments = this::onPreviewAttachments, onUserDataClicked = this::onUserDataClicked, + onLinkClicked = { onLinkClicked(context, it) }, onSendLocationClicked = this::onSendLocationClicked, onCreatePollClicked = this::onCreatePollClicked, onJoinCallClicked = this::onJoinCallClicked, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 4c92585b0d..4edc943a73 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -89,6 +89,7 @@ import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -273,6 +274,7 @@ class MessagesPresenter @AssistedInject constructor( ) = launch { when (action) { TimelineItemAction.Copy -> handleCopyContents(targetEvent) + TimelineItemAction.CopyLink -> handleCopyLink(targetEvent) TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting) TimelineItemAction.Reply, @@ -435,6 +437,20 @@ class MessagesPresenter @AssistedInject constructor( event.eventId?.let { timelineState.eventSink(TimelineEvents.PollEndClicked(it)) } } + private suspend fun handleCopyLink(event: TimelineItem.Event) { + event.eventId ?: return + room.getPermalinkFor(event.eventId).fold( + onSuccess = { permalink -> + clipboardHelper.copyPlainText(permalink) + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard)) + }, + onFailure = { + Timber.e(it, "Failed to get permalink for event ${event.eventId}") + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error)) + } + ) + } + private suspend fun handleCopyContents(event: TimelineItem.Event) { val content = when (event.content) { is TimelineItemTextBasedContent -> event.content.body diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 91cccd88e8..9cf4375769 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -119,6 +119,7 @@ fun MessagesView( onRoomDetailsClicked: () -> Unit, onEventClicked: (event: TimelineItem.Event) -> Boolean, onUserDataClicked: (UserId) -> Unit, + onLinkClicked: (String) -> Unit, onPreviewAttachments: (ImmutableList) -> Unit, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, @@ -213,6 +214,7 @@ fun MessagesView( onMessageClicked = ::onMessageClicked, onMessageLongClicked = ::onMessageLongClicked, onUserDataClicked = onUserDataClicked, + onLinkClicked = onLinkClicked, onTimestampClicked = { event -> if (event.localSendState is LocalEventSendState.SendingFailed) { state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event)) @@ -313,6 +315,7 @@ private fun MessagesViewContent( state: MessagesState, onMessageClicked: (TimelineItem.Event) -> Unit, onUserDataClicked: (UserId) -> Unit, + onLinkClicked: (String) -> Unit, onReactionClicked: (key: String, TimelineItem.Event) -> Unit, onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit, onMoreReactionsClicked: (TimelineItem.Event) -> Unit, @@ -386,6 +389,7 @@ private fun MessagesViewContent( onMessageClicked = onMessageClicked, onMessageLongClicked = onMessageLongClicked, onUserDataClicked = onUserDataClicked, + onLinkClicked = onLinkClicked, onTimestampClicked = onTimestampClicked, onReactionClicked = onReactionClicked, onReactionLongClicked = onReactionLongClicked, @@ -570,6 +574,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onEventClicked = { false }, onPreviewAttachments = {}, onUserDataClicked = {}, + onLinkClicked = {}, onSendLocationClicked = {}, onCreatePollClicked = {}, onJoinCallClicked = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index f836e2f8a0..5ebedeb06f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -96,6 +96,7 @@ class ActionListPresenter @Inject constructor( is TimelineItemStateContent -> { buildList { add(TimelineItemAction.Copy) + add(TimelineItemAction.CopyLink) if (isDeveloperModeEnabled) { add(TimelineItemAction.ViewSource) } @@ -119,6 +120,7 @@ class ActionListPresenter @Inject constructor( if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.Copy) } + add(TimelineItemAction.CopyLink) if (isDeveloperModeEnabled) { add(TimelineItemAction.ViewSource) } @@ -136,6 +138,7 @@ class ActionListPresenter @Inject constructor( add(TimelineItemAction.Reply) add(TimelineItemAction.Forward) } + add(TimelineItemAction.CopyLink) if (isDeveloperModeEnabled) { add(TimelineItemAction.ViewSource) } @@ -176,6 +179,7 @@ class ActionListPresenter @Inject constructor( if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.Copy) } + add(TimelineItemAction.CopyLink) if (isDeveloperModeEnabled) { add(TimelineItemAction.ViewSource) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 10952058b5..be78037d76 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -135,6 +135,7 @@ fun aTimelineItemActionList(): ImmutableList { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.Edit, TimelineItemAction.Redact, TimelineItemAction.ReportContent, @@ -146,6 +147,7 @@ fun aTimelineItemPollActionList(): ImmutableList { TimelineItemAction.EndPoll, TimelineItemAction.Reply, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index f244f515f3..f61e6197c2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -31,6 +31,7 @@ sealed class TimelineItemAction( ) { data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward) data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy) + data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link) data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true) data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply) data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 37e404d314..04c182a566 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -49,6 +49,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.Mention import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder @@ -96,6 +97,8 @@ class MessageComposerPresenter @Inject constructor( private val messageComposerContext: MessageComposerContextImpl, private val richTextEditorStateFactory: RichTextEditorStateFactory, private val currentSessionIdHolder: CurrentSessionIdHolder, + private val permalinkParser: PermalinkParser, + private val permalinkBuilder: PermalinkBuilder, permissionsPresenterFactory: PermissionsPresenter.Factory, ) : Presenter { private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA) @@ -334,7 +337,7 @@ class MessageComposerPresenter @Inject constructor( } is MentionSuggestion.Member -> { val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value - val link = PermalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch + val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch richTextEditorState.insertMentionAtSuggestion(text = text, link = link) } } @@ -345,6 +348,7 @@ class MessageComposerPresenter @Inject constructor( return MessageComposerState( richTextEditorState = richTextEditorState, + permalinkParser = permalinkParser, isFullScreen = isFullScreen.value, mode = messageComposerContext.composerMode, showAttachmentSourcePicker = showAttachmentSourcePicker, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 09eb51477f..194ce1914c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Stable import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.mentions.MentionSuggestion import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList @@ -28,6 +29,7 @@ import kotlinx.collections.immutable.ImmutableList @Stable data class MessageComposerState( val richTextEditorState: RichTextEditorState, + val permalinkParser: PermalinkParser, val isFullScreen: Boolean, val mode: MessageComposerMode, val showAttachmentSourcePicker: Boolean, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index da3c0c8af7..fb7f616e12 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -16,9 +16,12 @@ package io.element.android.features.messages.impl.messagecomposer +import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.mentions.MentionSuggestion import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList @@ -43,6 +46,10 @@ fun aMessageComposerState( memberSuggestions: ImmutableList = persistentListOf(), ) = MessageComposerState( richTextEditorState = richTextEditorState, + permalinkParser = object : PermalinkParser { + override fun parse(uriString: String): PermalinkData = TODO() + override fun parse(uri: Uri): PermalinkData = TODO() + }, isFullScreen = isFullScreen, mode = mode, showTextFormatting = showTextFormatting, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 6cb2900a20..e68cf29844 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -109,6 +109,7 @@ internal fun MessageComposerView( modifier = modifier, state = state.richTextEditorState, voiceMessageState = voiceMessageState.voiceMessageState, + permalinkParser = state.permalinkParser, subcomposing = subcomposing, onRequestFocus = ::onRequestFocus, onSendMessage = ::sendMessage, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt index aefadcbb25..26ad72dcf9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt @@ -28,6 +28,7 @@ import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider import io.element.android.wysiwyg.compose.StyledHtmlConverter @@ -39,7 +40,9 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) @SingleIn(SessionScope::class) -class DefaultHtmlConverterProvider @Inject constructor() : HtmlConverterProvider { +class DefaultHtmlConverterProvider @Inject constructor( + private val permalinkParser: PermalinkParser, +) : HtmlConverterProvider { private val htmlConverter: MutableState = mutableStateOf(null) @Composable @@ -50,7 +53,10 @@ class DefaultHtmlConverterProvider @Inject constructor() : HtmlConverterProvider } val editorStyle = ElementRichTextEditorStyle.textStyle() - val mentionSpanProvider = rememberMentionSpanProvider(currentUserId = currentUserId) + val mentionSpanProvider = rememberMentionSpanProvider( + currentUserId = currentUserId, + permalinkParser = permalinkParser, + ) val context = LocalContext.current diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 67915bf9d4..3cd525c614 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -33,7 +33,6 @@ import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction @@ -41,15 +40,11 @@ import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.encryption.BackupState -import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin -import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope @@ -68,8 +63,6 @@ class TimelinePresenter @AssistedInject constructor( private val dispatchers: CoroutineDispatchers, private val appScope: CoroutineScope, @Assisted private val navigator: MessagesNavigator, - private val verificationService: SessionVerificationService, - private val encryptionService: EncryptionService, private val redactedVoiceMessageManager: RedactedVoiceMessageManager, private val sendPollResponseAction: SendPollResponseAction, private val endPollAction: EndPollAction, @@ -101,21 +94,9 @@ class TimelinePresenter @AssistedInject constructor( val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } val newItemState = remember { mutableStateOf(NewEventState.None) } - val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState() - val keyBackupState by encryptionService.backupStateStateFlow.collectAsState() - val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true) val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true) - val sessionState by remember { - derivedStateOf { - SessionState( - isSessionVerified = sessionVerifiedStatus == SessionVerifiedStatus.Verified, - isKeyBackupEnabled = keyBackupState == BackupState.ENABLED - ) - } - } - fun handleEvents(event: TimelineEvents) { when (event) { TimelineEvents.LoadMore -> localScope.paginateBackwards() @@ -184,7 +165,6 @@ class TimelinePresenter @AssistedInject constructor( timelineItems = timelineItems, renderReadReceipts = renderReadReceipts, newEventState = newItemState.value, - sessionState = sessionState, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index ab9d93d678..4e2f9b8d42 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import kotlinx.collections.immutable.ImmutableList @@ -32,7 +31,6 @@ data class TimelineState( val highlightedEventId: EventId?, val paginationState: MatrixTimeline.PaginationState, val newEventState: NewEventState, - val sessionState: SessionState, val eventSink: (TimelineEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index aa35b620b9..ae7f62ebd7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -29,7 +29,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel -import io.element.android.features.messages.impl.timeline.session.aSessionState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.EventId @@ -58,10 +57,6 @@ fun aTimelineState( renderReadReceipts = renderReadReceipts, highlightedEventId = null, newEventState = NewEventState.None, - sessionState = aSessionState( - isSessionVerified = true, - isKeyBackupEnabled = true, - ), eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 094ecb6c3b..56a2676f43 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -81,6 +81,7 @@ fun TimelineView( typingNotificationState: TypingNotificationState, roomName: String?, onUserDataClicked: (UserId) -> Unit, + onLinkClicked: (String) -> Unit, onMessageClicked: (TimelineItem.Event) -> Unit, onMessageLongClicked: (TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, @@ -140,13 +141,13 @@ fun TimelineView( onClick = onMessageClicked, onLongClick = onMessageLongClicked, onUserDataClick = onUserDataClicked, + onLinkClicked = onLinkClicked, inReplyToClick = ::inReplyToClicked, onReactionClick = onReactionClicked, onReactionLongClick = onReactionLongClicked, onMoreReactionsClick = onMoreReactionsClicked, onReadReceiptClick = onReadReceiptClick, onTimestampClicked = onTimestampClicked, - sessionState = state.sessionState, eventSink = state.eventSink, onSwipeToReply = onSwipeToReply, ) @@ -276,6 +277,7 @@ internal fun TimelineViewPreview( onMessageClicked = {}, onTimestampClicked = {}, onUserDataClicked = {}, + onLinkClicked = {}, onMessageLongClicked = {}, onReactionClicked = { _, _ -> }, onReactionLongClicked = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index a86095b3fc..92b12b2dc8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -38,6 +38,7 @@ internal fun ATimelineItemEventRow( onClick = {}, onLongClick = {}, onUserDataClick = {}, + onLinkClicked = {}, inReplyToClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 81c10ad7d8..22bce0e873 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -17,7 +17,6 @@ package io.element.android.features.messages.impl.timeline.components import android.annotation.SuppressLint -import android.net.Uri import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -50,7 +49,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.res.stringResource @@ -94,7 +92,6 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo import io.element.android.features.messages.impl.timeline.model.metadata -import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -109,9 +106,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.permalink.PermalinkData -import io.element.android.libraries.matrix.api.permalink.PermalinkParser -import io.element.android.libraries.matrix.api.room.Mention import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings @@ -128,6 +122,7 @@ fun TimelineItemEventRow( isHighlighted: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, + onLinkClicked: (String) -> Unit, onUserDataClick: (UserId) -> Unit, inReplyToClick: (EventId) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, @@ -151,13 +146,6 @@ fun TimelineItemEventRow( inReplyToClick(inReplyToEventId) } - fun onMentionClicked(mention: Mention) { - when (mention) { - is Mention.User -> onUserDataClick(mention.userId) - else -> Unit // TODO implement actions for other mentions being clicked - } - } - Column(modifier = modifier.fillMaxWidth()) { if (event.groupPosition.isNew()) { Spacer(modifier = Modifier.height(16.dp)) @@ -203,7 +191,7 @@ fun TimelineItemEventRow( onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) }, - onMentionClicked = ::onMentionClicked, + onLinkClicked = onLinkClicked, eventSink = eventSink, ) } @@ -222,7 +210,7 @@ fun TimelineItemEventRow( onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) }, - onMentionClicked = ::onMentionClicked, + onLinkClicked = onLinkClicked, eventSink = eventSink, ) } @@ -278,7 +266,7 @@ private fun TimelineItemEventRowContent( onReactionClicked: (emoji: String) -> Unit, onReactionLongClicked: (emoji: String) -> Unit, onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit, - onMentionClicked: (Mention) -> Unit, + onLinkClicked: (String) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, modifier: Modifier = Modifier, ) { @@ -346,7 +334,7 @@ private fun TimelineItemEventRowContent( onTimestampClicked = { onTimestampClicked(event) }, - onMentionClicked = onMentionClicked, + onLinkClicked = onLinkClicked, eventSink = eventSink, ) } @@ -429,7 +417,7 @@ private fun MessageEventBubbleContent( @Suppress("UNUSED_PARAMETER") inReplyToClick: () -> Unit, onTimestampClicked: () -> Unit, - onMentionClicked: (Mention) -> Unit, + onLinkClicked: (String) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, @SuppressLint("ModifierParameter") // need to rename this modifier to prevent linter false positives @@ -530,7 +518,6 @@ private fun MessageEventBubbleContent( modifier: Modifier = Modifier, canShrinkContent: Boolean = false, ) { - val context = LocalContext.current val timestampLayoutModifier: Modifier val contentModifier: Modifier when { @@ -566,20 +553,7 @@ private fun MessageEventBubbleContent( ) { onContentLayoutChanged -> TimelineItemEventContentView( content = event.content, - onLinkClicked = { url -> - when (val permalink = PermalinkParser.parse(Uri.parse(url))) { - is PermalinkData.UserLink -> { - onMentionClicked(Mention.User(UserId(permalink.userId))) - } - is PermalinkData.RoomLink -> { - onMentionClicked(Mention.Room(permalink.getRoomId(), permalink.getRoomAlias())) - } - is PermalinkData.FallbackLink, - is PermalinkData.RoomEmailInviteLink -> { - context.openUrlInExternalApp(url) - } - } - }, + onLinkClicked = onLinkClicked, eventSink = eventSink, onContentLayoutChanged = onContentLayoutChanged, modifier = contentModifier diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index 74998afb89..a5d42d5dbf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -32,8 +32,6 @@ import io.element.android.features.messages.impl.timeline.components.group.Group import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.session.SessionState -import io.element.android.features.messages.impl.timeline.session.aSessionState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.EventId @@ -46,11 +44,11 @@ fun TimelineItemGroupedEventsRow( renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, highlightedItem: String?, - sessionState: SessionState, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, onUserDataClick: (UserId) -> Unit, + onLinkClicked: (String) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, @@ -73,11 +71,11 @@ fun TimelineItemGroupedEventsRow( highlightedItem = highlightedItem, renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, - sessionState = sessionState, onClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, + onLinkClicked = onLinkClicked, onTimestampClicked = onTimestampClicked, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, @@ -97,11 +95,11 @@ private fun TimelineItemGroupedEventsRowContent( highlightedItem: String?, renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, - sessionState: SessionState, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, onUserDataClick: (UserId) -> Unit, + onLinkClicked: (String) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, @@ -130,11 +128,11 @@ private fun TimelineItemGroupedEventsRowContent( renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, highlightedItem = highlightedItem, - sessionState = sessionState, onClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, + onLinkClicked = onLinkClicked, onTimestampClicked = onTimestampClicked, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, @@ -170,11 +168,11 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi highlightedItem = null, renderReadReceipts = true, isLastOutgoingMessage = false, - sessionState = aSessionState(), onClick = {}, onLongClick = {}, inReplyToClick = {}, onUserDataClick = {}, + onLinkClicked = {}, onTimestampClicked = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, @@ -195,11 +193,11 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi highlightedItem = null, renderReadReceipts = true, isLastOutgoingMessage = false, - sessionState = aSessionState(), onClick = {}, onLongClick = {}, inReplyToClick = {}, onUserDataClick = {}, + onLinkClicked = {}, onTimestampClicked = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt index b91699a5ee..ba357ab573 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemReactionsView.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -166,6 +167,7 @@ internal fun TimelineItemReactionsViewOutgoingPreview() = ElementPreview { ) } +@ExcludeFromCoverage @Composable private fun ContentToPreview( reactions: ImmutableList, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 6acddd179d..89b223dafe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -23,7 +23,6 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent -import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -34,8 +33,8 @@ internal fun TimelineItemRow( renderReadReceipts: Boolean, isLastOutgoingMessage: Boolean, highlightedItem: String?, - sessionState: SessionState, onUserDataClick: (UserId) -> Unit, + onLinkClicked: (String) -> Unit, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, @@ -52,7 +51,6 @@ internal fun TimelineItemRow( is TimelineItem.Virtual -> { TimelineItemVirtualRow( virtual = timelineItem, - sessionState = sessionState, modifier = modifier, ) } @@ -79,6 +77,7 @@ internal fun TimelineItemRow( onClick = { onClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) }, onUserDataClick = onUserDataClick, + onLinkClicked = onLinkClicked, inReplyToClick = inReplyToClick, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, @@ -98,11 +97,11 @@ internal fun TimelineItemRow( renderReadReceipts = renderReadReceipts, isLastOutgoingMessage = isLastOutgoingMessage, highlightedItem = highlightedItem, - sessionState = sessionState, onClick = onClick, onLongClick = onLongClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, + onLinkClicked = onLinkClicked, onTimestampClicked = onTimestampClicked, onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, 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 5ef3c2ef2c..306c11aaba 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 @@ -25,17 +25,15 @@ 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.TimelineItemReadMarkerModel -import io.element.android.features.messages.impl.timeline.session.SessionState @Composable fun TimelineItemVirtualRow( virtual: TimelineItem.Virtual, - sessionState: SessionState, modifier: Modifier = Modifier ) { when (virtual.model) { is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier) TimelineItemReadMarkerModel -> TimelineItemReadMarkerView() - is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(sessionState, modifier) + is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/ExtraPadding.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/ExtraPadding.kt deleted file mode 100644 index c9706f06ac..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/ExtraPadding.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.messages.impl.timeline.components.event - -import androidx.compose.material3.LocalTextStyle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextMeasurer -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.rememberTextMeasurer -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState -import io.element.android.libraries.ui.strings.CommonStrings -import kotlin.math.ceil - -// Allow to not overlap the timestamp with the text, in the message bubble. -// Compute the size of the worst case. -data class ExtraPadding(val extraWidth: Dp) - -val noExtraPadding = ExtraPadding(0.dp) - -/** - * See [io.element.android.features.messages.impl.timeline.components.TimelineEventTimestampView] for the related View. - * And https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1819%253A99506 for the design. - */ -@Composable -fun TimelineItem.Event.toExtraPadding(): ExtraPadding { - val formattedTime = sentTime - val hasMessageSendingFailed = localSendState is LocalEventSendState.SendingFailed - val isMessageEdited = (content as? TimelineItemTextBasedContent)?.isEdited.orFalse() - - val textMeasurer = rememberTextMeasurer(cacheSize = 128) - val density = LocalDensity.current - - var strLen = 2.dp // Extra space char - if (isMessageEdited) { - val editedText = stringResource(id = CommonStrings.common_edited_suffix) - val extraLen = remember(editedText, density) { textMeasurer.getExtraPadding(editedText, density) } + 10.dp // Text + spacing - strLen += extraLen - } - strLen += remember(formattedTime, density) { textMeasurer.getExtraPadding(formattedTime, density) } - if (hasMessageSendingFailed) { - strLen += 19.dp // Image + spacing - // I do not know why, but adding extra widths avoid overlapping when the - // message is edited and in error. - if (isMessageEdited) { - strLen += 2.dp - } - } - return ExtraPadding(strLen) -} - -private fun TextMeasurer.getExtraPadding(text: String, density: Density): Dp { - val timestampTextStyle = ElementTheme.typography.fontBodyXsRegular - val textWidth = measure(text = text, style = timestampTextStyle).size.width - return (textWidth / density.density).dp -} - -/** - * Get a string to add to the content of the message to avoid overlapping the timestamp. - */ -@Composable -fun ExtraPadding.getStr(textStyle: TextStyle = LocalTextStyle.current): String { - if (extraWidth == 0.dp) return "" - val density = LocalDensity.current - val textMeasurer = rememberTextMeasurer(128) - val charWidth = remember(textStyle) { textMeasurer.measure(text = "\u00A0", style = textStyle).size.width } - val widthPx = remember(density, extraWidth) { with(density) { extraWidth.toPx() } } - // A space and some unbreakable spaces, always rounding the result to the next value if not a integer - return " " + "\u00A0".repeat(ceil(widthPx / charWidth).toInt()) -} - -@Composable -fun ExtraPadding.getDpSize(): Dp { - return extraWidth -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt index 77be3815e5..77854773c4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt @@ -20,7 +20,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem sealed interface RetrySendMenuEvents { data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents - data object RetrySend : RetrySendMenuEvents - data object RemoveFailed : RetrySendMenuEvents + data object Retry : RetrySendMenuEvents + data object Remove : RetrySendMenuEvents data object Dismiss : RetrySendMenuEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt index bc1415829f..8745df9e53 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt @@ -41,7 +41,7 @@ class RetrySendMenuPresenter @Inject constructor( is RetrySendMenuEvents.EventSelected -> { selectedEvent = event.event } - RetrySendMenuEvents.RetrySend -> { + RetrySendMenuEvents.Retry -> { coroutineScope.launch { selectedEvent?.transactionId?.let { transactionId -> room.retrySendMessage(transactionId) @@ -49,7 +49,7 @@ class RetrySendMenuPresenter @Inject constructor( selectedEvent = null } } - RetrySendMenuEvents.RemoveFailed -> { + RetrySendMenuEvents.Remove -> { coroutineScope.launch { selectedEvent?.transactionId?.let { transactionId -> room.cancelSend(transactionId) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt index 1eec2547cd..ce01c18dfa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt @@ -17,7 +17,6 @@ package io.element.android.features.messages.impl.timeline.components.retrysendmenu import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -54,18 +53,18 @@ internal fun RetrySendMessageMenu( } fun onRetry() { - state.eventSink(RetrySendMenuEvents.RetrySend) + state.eventSink(RetrySendMenuEvents.Retry) } - fun onRemoveFailed() { - state.eventSink(RetrySendMenuEvents.RemoveFailed) + fun onRemove() { + state.eventSink(RetrySendMenuEvents.Remove) } RetrySendMessageMenuBottomSheet( modifier = modifier, isVisible = isVisible, onRetry = ::onRetry, - onRemoveFailed = ::onRemoveFailed, + onRemove = ::onRemove, onDismiss = ::onDismiss ) } @@ -75,7 +74,7 @@ internal fun RetrySendMessageMenu( private fun RetrySendMessageMenuBottomSheet( isVisible: Boolean, onRetry: () -> Unit, - onRemoveFailed: () -> Unit, + onRemove: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { @@ -95,7 +94,10 @@ private fun RetrySendMessageMenuBottomSheet( } } ) { - RetrySendMenuContents(onRetry = onRetry, onRemoveFailed = onRemoveFailed) + RetrySendMenuContents( + onRetry = onRetry, + onRemove = onRemove, + ) // FIXME remove after https://issuetracker.google.com/issues/275849044 Spacer(modifier = Modifier.height(32.dp)) } @@ -106,7 +108,7 @@ private fun RetrySendMessageMenuBottomSheet( @Composable private fun ColumnScope.RetrySendMenuContents( onRetry: () -> Unit, - onRemoveFailed: () -> Unit, + onRemove: () -> Unit, sheetState: SheetState = rememberModalBottomSheetState(), ) { val coroutineScope = rememberCoroutineScope() @@ -142,22 +144,16 @@ private fun ColumnScope.RetrySendMenuContents( modifier = Modifier.clickable { coroutineScope.launch { sheetState.hide() - onRemoveFailed() + onRemove() } } ) } -@Suppress("UNUSED_PARAMETER") -@OptIn(ExperimentalMaterial3Api::class) @PreviewsDayNight @Composable internal fun RetrySendMessageMenuPreview(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) = ElementPreview { - // TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed - Column { - RetrySendMenuContents( - onRetry = {}, - onRemoveFailed = {}, - ) - } + RetrySendMessageMenu( + state = state, + ) } 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 index c555352864..db0dd1ae9a 100644 --- 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 @@ -16,7 +16,6 @@ package io.element.android.features.messages.impl.timeline.components.virtual -import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement @@ -29,20 +28,16 @@ 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.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.R -import io.element.android.features.messages.impl.timeline.session.SessionState -import io.element.android.features.messages.impl.timeline.session.SessionStateProvider 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( - sessionState: SessionState, modifier: Modifier = Modifier, ) { Row( @@ -61,26 +56,15 @@ fun TimelineEncryptedHistoryBannerView( tint = ElementTheme.colors.iconInfoPrimary ) Text( - text = stringResource(sessionState.toStringResId()), + text = stringResource(R.string.screen_room_encrypted_history_banner), style = ElementTheme.typography.fontBodyMdMedium, color = ElementTheme.colors.textInfoPrimary ) } } -@StringRes -private fun SessionState.toStringResId(): Int { - return when { - isSessionVerified.not() -> R.string.screen_room_encrypted_history_banner_unverified - isKeyBackupEnabled.not() -> R.string.screen_room_encrypted_history_banner - else -> R.string.screen_room_encrypted_history_banner // TODO strings need to be updated - } -} - @PreviewsDayNight @Composable -internal fun EncryptedHistoryBannerViewPreview( - @PreviewParameter(SessionStateProvider::class) sessionState: SessionState, -) = ElementPreview { - TimelineEncryptedHistoryBannerView(sessionState = sessionState) +internal fun EncryptedHistoryBannerViewPreview() = ElementPreview { + TimelineEncryptedHistoryBannerView() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index af1ef0bc3a..acd99d3704 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -41,6 +41,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType @@ -67,6 +68,7 @@ class TimelineItemContentMessageFactory @Inject constructor( private val fileExtensionExtractor: FileExtensionExtractor, private val featureFlagService: FeatureFlagService, private val htmlConverterProvider: HtmlConverterProvider, + private val permalinkParser: PermalinkParser, ) { suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent { return when (val messageType = content.type) { @@ -74,7 +76,10 @@ class TimelineItemContentMessageFactory @Inject constructor( val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}" TimelineItemEmoteContent( body = emoteBody, - htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"), + htmlDocument = messageType.formatted?.toHtmlDocument( + permalinkParser = permalinkParser, + prefix = "* $senderDisplayName", + ), formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(), isEdited = content.isEdited, ) @@ -197,7 +202,7 @@ class TimelineItemContentMessageFactory @Inject constructor( val body = messageType.body.trimEnd() TimelineItemNoticeContent( body = body, - htmlDocument = messageType.formatted?.toHtmlDocument(), + htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser), formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(), isEdited = content.isEdited, ) @@ -206,7 +211,7 @@ class TimelineItemContentMessageFactory @Inject constructor( val body = messageType.body.trimEnd() TimelineItemTextContent( body = body, - htmlDocument = messageType.formatted?.toHtmlDocument(), + htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser), formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(), isEdited = content.isEdited, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index 38125dcef7..0522379f72 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails @@ -43,6 +44,7 @@ class TimelineItemEventFactory @Inject constructor( private val contentFactory: TimelineItemContentFactory, private val matrixClient: MatrixClient, private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, + private val permalinkParser: PermalinkParser, ) { suspend fun create( currentTimelineItem: MatrixTimelineItem.Event, @@ -80,7 +82,7 @@ class TimelineItemEventFactory @Inject constructor( reactionsState = currentTimelineItem.computeReactionsState(), readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers), localSendState = currentTimelineItem.event.localSendState, - inReplyTo = currentTimelineItem.event.inReplyTo()?.map(), + inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser), isThreaded = currentTimelineItem.event.isThreaded(), debugInfo = currentTimelineItem.event.debugInfo, origin = currentTimelineItem.event.origin, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt index 759ead5b61..3c629f23fc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent @@ -34,7 +35,9 @@ data class InReplyToDetails( val textContent: String?, ) -fun InReplyTo.map() = when (this) { +fun InReplyTo.map( + permalinkParser: PermalinkParser, +) = when (this) { is InReplyTo.Ready -> InReplyToDetails( eventId = eventId, senderId = senderId, @@ -44,7 +47,7 @@ fun InReplyTo.map() = when (this) { textContent = when (content) { is MessageContent -> { val messageContent = content as MessageContent - (messageContent.type as? TextMessageType)?.toPlainText() ?: messageContent.body + (messageContent.type as? TextMessageType)?.toPlainText(permalinkParser = permalinkParser) ?: messageContent.body } is StickerContent -> { val stickerContent = content as StickerContent diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionStateProvider.kt deleted file mode 100644 index 2dd27d5456..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/session/SessionStateProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.messages.impl.timeline.session - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -open class SessionStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aSessionState(isSessionVerified = false, isKeyBackupEnabled = false), - aSessionState(isSessionVerified = true, isKeyBackupEnabled = false), - aSessionState(isSessionVerified = true, isKeyBackupEnabled = true), - ) -} - -internal fun aSessionState( - isSessionVerified: Boolean = false, - isKeyBackupEnabled: Boolean = false, -) = SessionState( - isSessionVerified = isSessionVerified, - isKeyBackupEnabled = isKeyBackupEnabled, -) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt index 2fea39eeb7..3b09afb7fc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt @@ -35,6 +35,7 @@ internal fun MessagesViewWithTypingPreview( onEventClicked = { false }, onPreviewAttachments = {}, onUserDataClicked = {}, + onLinkClicked = {}, onSendLocationClicked = {}, onCreatePollClicked = {}, onJoinCallClicked = {}, diff --git a/features/messages/impl/src/main/res/values-be/translations.xml b/features/messages/impl/src/main/res/values-be/translations.xml index c91bb96d04..f2ea6cfd09 100644 --- a/features/messages/impl/src/main/res/values-be/translations.xml +++ b/features/messages/impl/src/main/res/values-be/translations.xml @@ -5,24 +5,24 @@ "Ежа & Напоі" "Жывёлы & Прырода" "Аб\'екты" - "Усмешкі & Людзі" + "Усмешкі & Удзельнікі" "Падарожжы & Месцы" "Сімвалы" "Заблакіраваць карыстальніка" - "Адзначце, ці жадаеце вы схаваць усе бягучыя і будучыя паведамленні ад гэтага карыстальніка" + "Адзначце, ці хочаце вы схаваць усе бягучыя і будучыя паведамленні ад гэтага карыстальніка" "Гэтае паведамленне будзе перададзена адміністратару вашага хатняга сервера. Яны не змогуць прачытаць зашыфраваныя паведамленні." "Прычына, па якой вы паскардзіліся на гэты змест" "Камера" "Зрабіць фота" "Запісаць відэа" "Далучэнне" - "Фота & Відэа Бібліятэка" + "Бібліятэка фота & відэа" "Месцазнаходжанне" "Апытанне" "Фармаціраванне тэксту" "Гісторыя паведамленняў зараз недаступна." "Гісторыя паведамленняў у гэтым пакоі недаступная. Праверце гэтую прыладу, каб убачыць гісторыю паведамленняў." - "Вы жадаеце запрасіць іх назад?" + "Вы хочаце запрасіць іх назад?" "Вы адзін у гэтым чаце" "Апавясціць увесь пакой" "Усе" @@ -39,7 +39,7 @@ "Новы" "%1$d змена ў пакоі" - "%1$d змен у пакоі" + "%1$d змены ў пакоі" "%1$d змен у пакоі" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 30413d1b9f..ab604e468e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -76,11 +76,11 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember -import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender @@ -231,6 +231,27 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle action copy link`() = runTest { + val clipboardHelper = FakeClipboardHelper() + val event = aMessageEvent() + val matrixRoom = FakeMatrixRoom( + permalinkResult = { Result.success("a link") }, + ) + val presenter = createMessagesPresenter( + clipboardHelper = clipboardHelper, + matrixRoom = matrixRoom, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + assertThat(clipboardHelper.clipboardContents).isEqualTo("a link") + } + } + @Test fun `present - handle action reply`() = runTest { val presenter = createMessagesPresenter() @@ -725,6 +746,8 @@ class MessagesPresenterTest { richTextEditorStateFactory = TestRichTextEditorStateFactory(), permissionsPresenterFactory = permissionsPresenterFactory, currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), + permalinkParser = FakePermalinkParser(), + permalinkBuilder = FakePermalinkBuilder(), ) val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( this, @@ -741,8 +764,6 @@ class MessagesPresenterTest { dispatchers = coroutineDispatchers, appScope = this, navigator = navigator, - encryptionService = FakeEncryptionService(), - verificationService = FakeSessionVerificationService(), redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), endPollAction = endPollAction, sendPollResponseAction = FakeSendPollResponseAction(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index b931dc9860..e176e20805 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -478,6 +478,7 @@ private fun AndroidComposeTestRule.setMessa onRoomDetailsClicked: () -> Unit = EnsureNeverCalled(), onEventClicked: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(), onUserDataClicked: (UserId) -> Unit = EnsureNeverCalledWithParam(), + onLinkClicked: (String) -> Unit = EnsureNeverCalledWithParam(), onPreviewAttachments: (ImmutableList) -> Unit = EnsureNeverCalledWithParam(), onSendLocationClicked: () -> Unit = EnsureNeverCalled(), onCreatePollClicked: () -> Unit = EnsureNeverCalled(), @@ -492,6 +493,7 @@ private fun AndroidComposeTestRule.setMessa onRoomDetailsClicked = onRoomDetailsClicked, onEventClicked = onEventClicked, onUserDataClicked = onUserDataClicked, + onLinkClicked = onLinkClicked, onPreviewAttachments = onPreviewAttachments, onSendLocationClicked = onSendLocationClicked, onCreatePollClicked = onCreatePollClicked, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 981e8fc8ac..c1062625f7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -153,6 +153,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, ) @@ -193,6 +194,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, ) @@ -232,6 +234,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, @@ -272,6 +275,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, @@ -315,6 +319,7 @@ class ActionListPresenterTest { TimelineItemAction.Forward, TimelineItemAction.Edit, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.Redact, ) @@ -357,6 +362,7 @@ class ActionListPresenterTest { TimelineItemAction.Forward, TimelineItemAction.Edit, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, ) ) @@ -396,6 +402,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.Redact, ) @@ -435,6 +442,7 @@ class ActionListPresenterTest { displayEmojiReactions = false, actions = persistentListOf( TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, ) ) @@ -473,6 +481,7 @@ class ActionListPresenterTest { displayEmojiReactions = false, actions = persistentListOf( TimelineItemAction.Copy, + TimelineItemAction.CopyLink, ) ) ) @@ -513,6 +522,7 @@ class ActionListPresenterTest { TimelineItemAction.Forward, TimelineItemAction.Edit, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -595,6 +605,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Edit, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -632,6 +643,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Edit, TimelineItemAction.EndPoll, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -668,6 +680,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.EndPoll, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -703,6 +716,7 @@ class ActionListPresenterTest { displayEmojiReactions = true, actions = persistentListOf( TimelineItemAction.Reply, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -738,6 +752,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt index 562424474c..db056dea19 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt @@ -41,6 +41,7 @@ import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope @@ -57,6 +58,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), featureFlagService = FakeFeatureFlagService(), htmlConverterProvider = FakeHtmlConverterProvider(), + permalinkParser = FakePermalinkParser(), ), redactedMessageFactory = TimelineItemContentRedactedFactory(), stickerFactory = TimelineItemContentStickerFactory( @@ -73,6 +75,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { ), matrixClient = matrixClient, lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), + permalinkParser = FakePermalinkParser(), ), virtualItemFactory = TimelineItemVirtualFactory( daySeparatorFactory = TimelineItemDaySeparatorFactory( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index 5e980514cb..290f297117 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.Mention @@ -60,6 +61,8 @@ import io.element.android.libraries.matrix.test.A_USER_ID_3 import io.element.android.libraries.matrix.test.A_USER_ID_4 import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.mediapickers.api.PickerProvider @@ -805,7 +808,14 @@ class MessageComposerPresenterTest { @Test fun `present - insertMention`() = runTest { - val presenter = createPresenter(this) + val presenter = createPresenter( + coroutineScope = this, + permalinkBuilder = FakePermalinkBuilder( + result = { + Result.success("https://matrix.to/#/${A_USER_ID_2.value}") + } + ) + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -941,6 +951,7 @@ class MessageComposerPresenterTest { mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor, snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher, permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(), + permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder() ) = MessageComposerPresenter( coroutineScope, room, @@ -955,6 +966,8 @@ class MessageComposerPresenterTest { TestRichTextEditorStateFactory(), currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter), + permalinkParser = FakePermalinkParser(), + permalinkBuilder = permalinkBuilder, ) private suspend fun ReceiveTurbine.awaitFirstItem(): T { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt index 276544a057..7411ceda1a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.test.junit4.createComposeRule import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -32,7 +33,9 @@ class DefaultHtmlConverterProviderTest { @Test fun `calling provide without calling Update first should throw an exception`() { - val provider = DefaultHtmlConverterProvider() + val provider = DefaultHtmlConverterProvider( + permalinkParser = FakePermalinkParser(), + ) val exception = runCatching { provider.provide() }.exceptionOrNull() @@ -41,7 +44,9 @@ class DefaultHtmlConverterProviderTest { @Test fun `calling provide after calling Update first should return an HtmlConverter`() { - val provider = DefaultHtmlConverterProvider() + val provider = DefaultHtmlConverterProvider( + permalinkParser = FakePermalinkParser(), + ) composeTestRule.setContent { CompositionLocalProvider(LocalInspectionMode provides true) { provider.Update(currentUserId = A_USER_ID) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 5ee6ca8aa0..2485e57172 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -26,7 +26,6 @@ import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline @@ -47,13 +46,11 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem -import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitLastSequentialItem @@ -89,7 +86,6 @@ class TimelinePresenterTest { assertThat(initialState.timelineItems).isEmpty() val loadedNoTimelineState = awaitItem() assertThat(loadedNoTimelineState.timelineItems).isEmpty() - assertThat(loadedNoTimelineState.sessionState).isEqualTo(SessionState(isSessionVerified = false, isKeyBackupEnabled = false)) } } @@ -512,8 +508,6 @@ class TimelinePresenterTest { dispatchers = testCoroutineDispatchers(), appScope = this, navigator = messagesNavigator, - encryptionService = FakeEncryptionService(), - verificationService = FakeSessionVerificationService(), redactedVoiceMessageManager = redactedVoiceMessageManager, endPollAction = endPollAction, sendPollResponseAction = sendPollResponseAction, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index 78cd671e3f..44fb6270ae 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -45,6 +45,7 @@ class TimelineViewTest { typingNotificationState = aTypingNotificationState(), roomName = null, onUserDataClicked = EnsureNeverCalledWithParam(), + onLinkClicked = EnsureNeverCalledWithParam(), onMessageClicked = EnsureNeverCalledWithParam(), onMessageLongClicked = EnsureNeverCalledWithParam(), onTimestampClicked = EnsureNeverCalledWithParam(), @@ -72,6 +73,7 @@ class TimelineViewTest { typingNotificationState = aTypingNotificationState(), roomName = null, onUserDataClicked = EnsureNeverCalledWithParam(), + onLinkClicked = EnsureNeverCalledWithParam(), onMessageClicked = EnsureNeverCalledWithParam(), onMessageLongClicked = EnsureNeverCalledWithParam(), onTimestampClicked = EnsureNeverCalledWithParam(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt index afed2d8b8f..21bc5b53ac 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt @@ -43,7 +43,6 @@ class RetrySendMenuPresenterTests { val initialState = awaitItem() val selectedEvent = aTimelineItemEvent() initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) - assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent) } } @@ -57,8 +56,9 @@ class RetrySendMenuPresenterTests { val selectedEvent = aTimelineItemEvent() initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) skipItems(1) - initialState.eventSink(RetrySendMenuEvents.Dismiss) + assertThat(room.cancelSendCount).isEqualTo(0) + assertThat(room.retrySendMessageCount).isEqualTo(0) assertThat(awaitItem().selectedEvent).isNull() } } @@ -72,8 +72,8 @@ class RetrySendMenuPresenterTests { val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) skipItems(1) - - initialState.eventSink(RetrySendMenuEvents.RetrySend) + initialState.eventSink(RetrySendMenuEvents.Retry) + assertThat(room.cancelSendCount).isEqualTo(0) assertThat(room.retrySendMessageCount).isEqualTo(1) assertThat(awaitItem().selectedEvent).isNull() } @@ -88,8 +88,8 @@ class RetrySendMenuPresenterTests { val selectedEvent = aTimelineItemEvent(transactionId = null) initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) skipItems(1) - - initialState.eventSink(RetrySendMenuEvents.RetrySend) + initialState.eventSink(RetrySendMenuEvents.Retry) + assertThat(room.cancelSendCount).isEqualTo(0) assertThat(room.retrySendMessageCount).isEqualTo(0) assertThat(awaitItem().selectedEvent).isNull() } @@ -105,8 +105,8 @@ class RetrySendMenuPresenterTests { val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) skipItems(1) - - initialState.eventSink(RetrySendMenuEvents.RetrySend) + initialState.eventSink(RetrySendMenuEvents.Retry) + assertThat(room.cancelSendCount).isEqualTo(0) assertThat(room.retrySendMessageCount).isEqualTo(1) assertThat(awaitItem().selectedEvent).isNull() } @@ -121,9 +121,9 @@ class RetrySendMenuPresenterTests { val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) skipItems(1) - - initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + initialState.eventSink(RetrySendMenuEvents.Remove) assertThat(room.cancelSendCount).isEqualTo(1) + assertThat(room.retrySendMessageCount).isEqualTo(0) assertThat(awaitItem().selectedEvent).isNull() } } @@ -137,9 +137,9 @@ class RetrySendMenuPresenterTests { val selectedEvent = aTimelineItemEvent(transactionId = null) initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) skipItems(1) - - initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + initialState.eventSink(RetrySendMenuEvents.Remove) assertThat(room.cancelSendCount).isEqualTo(0) + assertThat(room.retrySendMessageCount).isEqualTo(0) assertThat(awaitItem().selectedEvent).isNull() } } @@ -154,9 +154,9 @@ class RetrySendMenuPresenterTests { val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) skipItems(1) - - initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + initialState.eventSink(RetrySendMenuEvents.Remove) assertThat(room.cancelSendCount).isEqualTo(1) + assertThat(room.retrySendMessageCount).isEqualTo(0) assertThat(awaitItem().selectedEvent).isNull() } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenuTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenuTest.kt new file mode 100644 index 0000000000..41d6d5610f --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenuTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.retrysendmenu + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class RetrySendMessageMenuTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `dismiss the bottom sheet emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setRetrySendMessageMenu( + aRetrySendMenuState( + event = aTimelineItemEvent(), + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + // Cannot test this for now. + // eventsRecorder.assertSingle(RetrySendMenuEvents.Dismiss) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `retry to send the event emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setRetrySendMessageMenu( + aRetrySendMenuState( + event = aTimelineItemEvent(), + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_room_retry_send_menu_send_again_action) + eventsRecorder.assertSingle(RetrySendMenuEvents.Retry) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `remove the event emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setRetrySendMessageMenu( + aRetrySendMenuState( + event = aTimelineItemEvent(), + eventSink = eventsRecorder + ), + ) + rule.clickOn(CommonStrings.action_remove) + eventsRecorder.assertSingle(RetrySendMenuEvents.Remove) + } +} + +private fun AndroidComposeTestRule.setRetrySendMessageMenu( + state: RetrySendMenuState, +) { + setContent { + RetrySendMessageMenu( + state = state, + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt index 98953f4827..6e475707ad 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt @@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import kotlinx.collections.immutable.persistentListOf @@ -664,6 +665,7 @@ class TimelineItemContentMessageFactoryTest { fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), featureFlagService = featureFlagService, htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform), + permalinkParser = FakePermalinkParser(), ) private fun createStickerContent( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt index f07a73fd84..bf287341ad 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt @@ -26,14 +26,27 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershi import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import org.junit.Test class InReplyToDetailTest { @Test fun `map - with a not ready InReplyTo does not work`() { - assertThat(InReplyTo.Pending.map()).isNull() - assertThat(InReplyTo.NotLoaded(AN_EVENT_ID).map()).isNull() - assertThat(InReplyTo.Error.map()).isNull() + assertThat( + InReplyTo.Pending.map( + permalinkParser = FakePermalinkParser() + ) + ).isNull() + assertThat( + InReplyTo.NotLoaded(AN_EVENT_ID).map( + permalinkParser = FakePermalinkParser() + ) + ).isNull() + assertThat( + InReplyTo.Error.map( + permalinkParser = FakePermalinkParser() + ) + ).isNull() } @Test @@ -48,7 +61,9 @@ class InReplyToDetailTest { change = MembershipChange.INVITED, ) ) - val inReplyToDetails = inReplyTo.map() + val inReplyToDetails = inReplyTo.map( + permalinkParser = FakePermalinkParser() + ) assertThat(inReplyToDetails).isNotNull() assertThat(inReplyToDetails?.textContent).isNull() } @@ -74,7 +89,11 @@ class InReplyToDetailTest { ) ) ) - assertThat(inReplyTo.map()?.textContent).isEqualTo("Hello!") + assertThat( + inReplyTo.map( + permalinkParser = FakePermalinkParser() + )?.textContent + ).isEqualTo("Hello!") } @Test @@ -95,6 +114,10 @@ class InReplyToDetailTest { ) ) ) - assertThat(inReplyTo.map()?.textContent).isEqualTo("**Hello!**") + assertThat( + inReplyTo.map( + permalinkParser = FakePermalinkParser() + )?.textContent + ).isEqualTo("**Hello!**") } } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerItem.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerItem.kt index 56dbaeb3ca..c82f1cb734 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerItem.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerItem.kt @@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.api.poll.PollAnswer * @property isSelected whether the user has selected this answer. * @property isEnabled whether the answer can be voted. * @property isWinner whether this is the winner answer in the poll. - * @property isDisclosed whether the votes for this answer should be disclosed. + * @property showVotes whether the votes for this answer should be displayed. * @property votesCount the number of votes for this answer. * @property percentage the percentage of votes for this answer. */ @@ -34,7 +34,7 @@ data class PollAnswerItem( val isSelected: Boolean, val isEnabled: Boolean, val isWinner: Boolean, - val isDisclosed: Boolean, + val showVotes: Boolean, val votesCount: Int, val percentage: Float, ) diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt index a84ef502ca..70658e65a8 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt @@ -41,6 +41,7 @@ import io.element.android.libraries.designsystem.theme.components.LinearProgress import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.toEnabledColor +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.ui.strings.CommonPlurals @Composable @@ -79,17 +80,36 @@ internal fun PollAnswerView( text = answerItem.answer.text, style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular, ) - if (answerItem.isDisclosed) { - Text( - modifier = Modifier.align(Alignment.Bottom), - text = pluralStringResource( - id = CommonPlurals.common_poll_votes_count, - count = answerItem.votesCount, - answerItem.votesCount - ), - style = if (answerItem.isWinner) ElementTheme.typography.fontBodySmMedium else ElementTheme.typography.fontBodySmRegular, - color = if (answerItem.isWinner) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary, + if (answerItem.showVotes) { + val text = pluralStringResource( + id = CommonPlurals.common_poll_votes_count, + count = answerItem.votesCount, + answerItem.votesCount ) + Row( + modifier = Modifier.align(Alignment.Bottom), + verticalAlignment = Alignment.CenterVertically, + ) { + if (answerItem.isWinner) { + Icon( + resourceId = CommonDrawables.ic_winner, + contentDescription = null, + tint = ElementTheme.colors.iconAccentTertiary, + ) + Spacer(modifier = Modifier.width(2.dp)) + Text( + text = text, + style = ElementTheme.typography.fontBodySmMedium, + color = ElementTheme.colors.textPrimary, + ) + } else { + Text( + text = text, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } + } } } Spacer(modifier = Modifier.height(10.dp)) @@ -98,7 +118,7 @@ internal fun PollAnswerView( color = if (answerItem.isWinner) ElementTheme.colors.textSuccessPrimary else answerItem.isEnabled.toEnabledColor(), progress = { when { - answerItem.isDisclosed -> answerItem.percentage + answerItem.showVotes -> answerItem.percentage answerItem.isSelected -> 1f else -> 0f } @@ -114,7 +134,7 @@ internal fun PollAnswerView( @Composable internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false), + answerItem = aPollAnswerItem(showVotes = true, isSelected = false), ) } @@ -122,7 +142,7 @@ internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview { @Composable internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true), + answerItem = aPollAnswerItem(showVotes = true, isSelected = true), ) } @@ -130,7 +150,7 @@ internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview { @Composable internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false), + answerItem = aPollAnswerItem(showVotes = false, isSelected = false), ) } @@ -138,7 +158,7 @@ internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview { @Composable internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true), + answerItem = aPollAnswerItem(showVotes = false, isSelected = true), ) } @@ -146,7 +166,7 @@ internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview { @Composable internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true), + answerItem = aPollAnswerItem(showVotes = true, isSelected = false, isEnabled = false, isWinner = true), ) } @@ -154,7 +174,7 @@ internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview { @Composable internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true), + answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = true), ) } @@ -162,6 +182,6 @@ internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview { @Composable internal fun PollAnswerEndedSelectedPreview() = ElementPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false), + answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = false), ) } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFixtures.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFixtures.kt index 206ee93ce0..9ec3ec3754 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFixtures.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFixtures.kt @@ -27,11 +27,11 @@ fun aPollQuestion() = "What type of food should we have at the party?" fun aPollAnswerItemList( hasVotes: Boolean = true, isEnded: Boolean = false, - isDisclosed: Boolean = true, + showVotes: Boolean = true, ) = persistentListOf( aPollAnswerItem( answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"), - isDisclosed = isDisclosed, + showVotes = showVotes, isEnabled = !isEnded, isWinner = isEnded, votesCount = if (hasVotes) 5 else 0, @@ -39,7 +39,7 @@ fun aPollAnswerItemList( ), aPollAnswerItem( answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"), - isDisclosed = isDisclosed, + showVotes = showVotes, isEnabled = !isEnded, isWinner = false, votesCount = 0, @@ -47,7 +47,7 @@ fun aPollAnswerItemList( ), aPollAnswerItem( answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"), - isDisclosed = isDisclosed, + showVotes = showVotes, isEnabled = !isEnded, isWinner = false, isSelected = true, @@ -55,7 +55,7 @@ fun aPollAnswerItemList( percentage = if (hasVotes) 0.1f else 0f ), aPollAnswerItem( - isDisclosed = isDisclosed, + showVotes = showVotes, isEnabled = !isEnded, votesCount = if (hasVotes) 4 else 0, percentage = if (hasVotes) 0.4f else 0f, @@ -70,7 +70,7 @@ fun aPollAnswerItem( isSelected: Boolean = false, isEnabled: Boolean = true, isWinner: Boolean = false, - isDisclosed: Boolean = true, + showVotes: Boolean = true, votesCount: Int = 4, percentage: Float = 0.4f, ) = PollAnswerItem( @@ -78,7 +78,7 @@ fun aPollAnswerItem( isSelected = isSelected, isEnabled = isEnabled, isWinner = isWinner, - isDisclosed = isDisclosed, + showVotes = showVotes, votesCount = votesCount, percentage = percentage ) @@ -87,14 +87,14 @@ fun aPollContentState( eventId: EventId? = null, isMine: Boolean = false, isEnded: Boolean = false, - isDisclosed: Boolean = true, + showVotes: Boolean = true, isPollEditable: Boolean = true, hasVotes: Boolean = true, question: String = aPollQuestion(), pollKind: PollKind = PollKind.Disclosed, answerItems: ImmutableList = aPollAnswerItemList( isEnded = isEnded, - isDisclosed = isDisclosed, + showVotes = showVotes, hasVotes = hasVotes ), ) = PollContentState( diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt index 3862c385dd..a77753fc4c 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentView.kt @@ -245,7 +245,7 @@ internal fun PollContentUndisclosedPreview() = ElementPreview { PollContentView( eventId = EventId("\$anEventId"), question = "What type of food should we have at the party?", - answerItems = aPollAnswerItemList(isDisclosed = false), + answerItems = aPollAnswerItemList(showVotes = false), pollKind = PollKind.Undisclosed, isPollEnded = false, isPollEditable = false, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt index 3cbe132c85..a661d7a1bc 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/model/DefaultPollContentStateFactory.kt @@ -60,7 +60,7 @@ class DefaultPollContentStateFactory @Inject constructor( isSelected = isSelected, isEnabled = !isPollEnded, isWinner = isWinner, - isDisclosed = content.kind.isDisclosed || isPollEnded, + showVotes = content.kind.isDisclosed || isPollEnded, votesCount = answerVoteCount, percentage = percentage, ) diff --git a/features/poll/impl/src/main/res/values-be/translations.xml b/features/poll/impl/src/main/res/values-be/translations.xml index 3749e1ec4c..1d0238bca3 100644 --- a/features/poll/impl/src/main/res/values-be/translations.xml +++ b/features/poll/impl/src/main/res/values-be/translations.xml @@ -4,11 +4,11 @@ "Паказаць вынікі толькі пасля заканчэння апытання" "Схаваць галасы" "Варыянт %1$d" - "Вашы змены не былі захаваны. Вы ўпэўнены, што жадаеце вярнуцца?" + "Вашы змены не былі захаваны. Вы ўпэўнены, што хочаце вярнуцца?" "Пытанне або тэма" "Пра што апытанне?" "Стварэнне апытання" - "Вы ўпэўнены, што жадаеце выдаліць гэтае апытанне?" + "Вы ўпэўнены, што хочаце выдаліць гэтае апытанне?" "Выдаліць апытанне" "Рэдагаваць апытанне" "Немагчыма знайсці бягучыя апытанні." diff --git a/features/poll/impl/src/main/res/values-hu/translations.xml b/features/poll/impl/src/main/res/values-hu/translations.xml index 74b83c8942..310c1a77db 100644 --- a/features/poll/impl/src/main/res/values-hu/translations.xml +++ b/features/poll/impl/src/main/res/values-hu/translations.xml @@ -4,7 +4,7 @@ "Eredmények megjelenítése csak a szavazás befejezése után" "Szavazatok elrejtése" "%1$d. lehetőség" - "A módosítások nem lettek mentve. Biztos, hogy vissza akar lépni?" + "A módosítások nem lettek mentve. Biztos, hogy visszalép?" "Kérdés vagy téma" "Miről szól ez a szavazás?" "Szavazás létrehozása" diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/pollcontent/PollContentStateFactoryTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/pollcontent/PollContentStateFactoryTest.kt index d39064d3a0..03058029a6 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/pollcontent/PollContentStateFactoryTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/pollcontent/PollContentStateFactoryTest.kt @@ -131,7 +131,7 @@ class PollContentStateFactoryTest { val state = factory.create(eventTimelineItem, aPollContent(PollKind.Undisclosed)) val expectedState = aPollContentState(pollKind = PollKind.Undisclosed).let { it.copy( - answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }.toImmutableList() + answerItems = it.answerItems.map { answerItem -> answerItem.copy(showVotes = false) }.toImmutableList() ) } assertThat(state).isEqualTo(expectedState) @@ -147,10 +147,10 @@ class PollContentStateFactoryTest { val expectedState = aPollContentState( pollKind = PollKind.Undisclosed, answerItems = listOf( - aPollAnswerItem(answer = A_POLL_ANSWER_1, isDisclosed = false, votesCount = 3, percentage = 0.3f), - aPollAnswerItem(answer = A_POLL_ANSWER_2, isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f), - aPollAnswerItem(answer = A_POLL_ANSWER_3, isDisclosed = false), - aPollAnswerItem(answer = A_POLL_ANSWER_4, isDisclosed = false, votesCount = 1, percentage = 0.1f), + aPollAnswerItem(answer = A_POLL_ANSWER_1, showVotes = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, showVotes = false, isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, showVotes = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, showVotes = false, votesCount = 1, percentage = 0.1f), ), ) assertThat(state).isEqualTo(expectedState) @@ -164,7 +164,7 @@ class PollContentStateFactoryTest { pollKind = PollKind.Undisclosed ).let { it.copy( - answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = true, isEnabled = false) }.toImmutableList(), + answerItems = it.answerItems.map { answerItem -> answerItem.copy(showVotes = true, isEnabled = false) }.toImmutableList(), ) } assertThat(state).isEqualTo(expectedState) @@ -258,7 +258,7 @@ class PollContentStateFactoryTest { isSelected: Boolean = false, isEnabled: Boolean = true, isWinner: Boolean = false, - isDisclosed: Boolean = true, + showVotes: Boolean = true, votesCount: Int = 0, percentage: Float = 0f, ) = PollAnswerItem( @@ -266,7 +266,7 @@ class PollContentStateFactoryTest { isSelected = isSelected, isEnabled = isEnabled, isWinner = isWinner, - isDisclosed = isDisclosed, + showVotes = showVotes, votesCount = votesCount, percentage = percentage, ) diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt index 0577412604..e488d911ed 100644 --- a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt +++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/PreferencesEntryPoint.kt @@ -45,7 +45,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onOpenBugReport() - fun onVerifyClicked() fun onSecureBackupClicked() fun onOpenRoomNotificationSettings(roomId: RoomId) } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index ee0658ddc6..edfb275f17 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -23,6 +23,11 @@ plugins { android { namespace = "io.element.android.features.preferences.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -44,12 +49,14 @@ dependencies { implementation(projects.libraries.pushstore.api) implementation(projects.libraries.indicator.api) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.troubleshoot.api) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(projects.libraries.matrixui) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) + implementation(projects.libraries.push.api) implementation(projects.features.rageshake.api) implementation(projects.features.lockscreen.api) implementation(projects.features.analytics.api) @@ -71,12 +78,14 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushstore.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) @@ -86,4 +95,6 @@ dependencies { testImplementation(projects.services.toolbox.test) testImplementation(projects.features.analytics.impl) testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index c27a5ec14e..b93e02dd39 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -24,6 +24,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -47,6 +48,7 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -54,6 +56,7 @@ class PreferencesFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val lockScreenEntryPoint: LockScreenEntryPoint, + private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint, private val logoutEntryPoint: LogoutEntryPoint, ) : BaseFlowNode( backstack = BackStack( @@ -85,6 +88,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object NotificationSettings : NavTarget + @Parcelize + data object TroubleshootNotifications : NavTarget + @Parcelize data object LockScreenSettings : NavTarget @@ -109,10 +115,6 @@ class PreferencesFlowNode @AssistedInject constructor( plugins().forEach { it.onOpenBugReport() } } - override fun onVerifyClicked() { - plugins().forEach { it.onVerifyClicked() } - } - override fun onSecureBackupClicked() { plugins().forEach { it.onSecureBackupClicked() } } @@ -177,9 +179,22 @@ class PreferencesFlowNode @AssistedInject constructor( override fun editDefaultNotificationMode(isOneToOne: Boolean) { backstack.push(NavTarget.EditDefaultNotificationSetting(isOneToOne)) } + + override fun onTroubleshootNotificationsClicked() { + backstack.push(NavTarget.TroubleshootNotifications) + } } createNode(buildContext, listOf(notificationSettingsCallback)) } + NavTarget.TroubleshootNotifications -> { + notificationTroubleShootEntryPoint.nodeBuilder(this, buildContext) + .callback(object : NotificationTroubleShootEntryPoint.Callback { + override fun onDone() { + backstack.pop() + } + }) + .build() + } is NavTarget.EditDefaultNotificationSetting -> { val callback = object : EditDefaultNotificationSettingNode.Callback { override fun openRoomNotificationSettings(roomId: RoomId) { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt index 122d13a817..621b0ed8b1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsNode.kt @@ -35,6 +35,7 @@ class NotificationSettingsNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun editDefaultNotificationMode(isOneToOne: Boolean) + fun onTroubleshootNotificationsClicked() } private val callbacks = plugins() @@ -43,6 +44,10 @@ class NotificationSettingsNode @AssistedInject constructor( callbacks.forEach { it.editDefaultNotificationMode(isOneToOne) } } + private fun onTroubleshootNotificationsClicked() { + callbacks.forEach { it.onTroubleshootNotificationsClicked() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -50,6 +55,7 @@ class NotificationSettingsNode @AssistedInject constructor( state = state, onOpenEditDefault = { openEditDefault(isOneToOne = it) }, onBackPressed = ::navigateUp, + onTroubleshootNotificationsClicked = ::onTroubleshootNotificationsClicked, modifier = modifier, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt index 7083b9f88b..9aa9cafb81 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt @@ -48,7 +48,7 @@ class NotificationSettingsPresenter @Inject constructor( ) : Presenter { @Composable override fun present(): NotificationSettingsState { - val userPushStore = remember { userPushStoreFactory.create(matrixClient.sessionId) } + val userPushStore = remember { userPushStoreFactory.getOrCreate(matrixClient.sessionId) } val systemNotificationsEnabled: MutableState = remember { mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled()) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt index 2dc0c02145..dc1e972aa6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt @@ -23,26 +23,48 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode open class NotificationSettingsStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aNotificationSettingsState(), - aNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading), - aNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))), + aValidNotificationSettingsState(), + aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Loading), + aValidNotificationSettingsState(changeNotificationSettingAction = AsyncAction.Failure(Throwable("error"))), + aInvalidNotificationSettingsState(), + aInvalidNotificationSettingsState(fixFailed = true), ) } -fun aNotificationSettingsState( +fun aValidNotificationSettingsState( changeNotificationSettingAction: AsyncAction = AsyncAction.Uninitialized, + atRoomNotificationsEnabled: Boolean = true, + callNotificationsEnabled: Boolean = true, + inviteForMeNotificationsEnabled: Boolean = true, + appNotificationEnabled: Boolean = true, + eventSink: (NotificationSettingsEvents) -> Unit = {}, ) = NotificationSettingsState( matrixSettings = NotificationSettingsState.MatrixSettings.Valid( - atRoomNotificationsEnabled = true, - callNotificationsEnabled = true, - inviteForMeNotificationsEnabled = true, + atRoomNotificationsEnabled = atRoomNotificationsEnabled, + callNotificationsEnabled = callNotificationsEnabled, + inviteForMeNotificationsEnabled = inviteForMeNotificationsEnabled, defaultGroupNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, defaultOneToOneNotificationMode = RoomNotificationMode.ALL_MESSAGES, ), appSettings = NotificationSettingsState.AppSettings( systemNotificationsEnabled = false, - appNotificationsEnabled = true, + appNotificationsEnabled = appNotificationEnabled, ), changeNotificationSettingAction = changeNotificationSettingAction, - eventSink = {} + eventSink = eventSink, +) + +fun aInvalidNotificationSettingsState( + fixFailed: Boolean = false, + eventSink: (NotificationSettingsEvents) -> Unit = {}, +) = NotificationSettingsState( + matrixSettings = NotificationSettingsState.MatrixSettings.Invalid( + fixFailed = fixFailed, + ), + appSettings = NotificationSettingsState.AppSettings( + systemNotificationsEnabled = false, + appNotificationsEnabled = true, + ), + changeNotificationSettingAction = AsyncAction.Uninitialized, + eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index e68b9fc8b5..d62f972d71 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -46,6 +46,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun NotificationSettingsView( state: NotificationSettingsState, onOpenEditDefault: (isOneToOne: Boolean) -> Unit, + onTroubleshootNotificationsClicked: () -> Unit, onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { @@ -77,6 +78,7 @@ fun NotificationSettingsView( // TODO We are removing the call notification toggle until support for call notifications has been added // onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) }, onInviteForMeNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) }, + onTroubleshootNotificationsClicked = onTroubleshootNotificationsClicked, ) } AsyncActionView( @@ -99,6 +101,7 @@ private fun NotificationSettingsContentView( // TODO We are removing the call notification toggle until support for call notifications has been added // onCallsNotificationsChanged: (Boolean) -> Unit, onInviteForMeNotificationsChanged: (Boolean) -> Unit, + onTroubleshootNotificationsClicked: () -> Unit, ) { val context = LocalContext.current if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) { @@ -163,6 +166,13 @@ private fun NotificationSettingsContentView( onCheckedChange = onInviteForMeNotificationsChanged ) } + PreferenceCategory(title = stringResource(id = R.string.troubleshoot_notifications_entry_point_section)) { + PreferenceText( + modifier = Modifier, + title = stringResource(id = R.string.troubleshoot_notifications_entry_point_title), + onClick = onTroubleshootNotificationsClicked + ) + } } } @@ -204,15 +214,6 @@ internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSetti state = state, onBackPressed = {}, onOpenEditDefault = {}, - ) -} - -@PreviewsDayNight -@Composable -internal fun InvalidNotificationSettingsViewPreview() = ElementPreview { - InvalidNotificationSettingsView( - showError = false, - onContinueClicked = {}, - onDismissError = {}, + onTroubleshootNotificationsClicked = {}, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index 3bd762794d..11f3fc3dd3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -44,7 +44,6 @@ class PreferencesRootNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun onOpenBugReport() - fun onVerifyClicked() fun onSecureBackupClicked() fun onOpenAnalytics() fun onOpenAbout() @@ -61,10 +60,6 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenBugReport() } } - private fun onVerifyClicked() { - plugins().forEach { it.onVerifyClicked() } - } - private fun onSecureBackupClicked() { plugins().forEach { it.onSecureBackupClicked() } } @@ -138,7 +133,6 @@ class PreferencesRootNode @AssistedInject constructor( onOpenRageShake = this::onOpenBugReport, onOpenAnalytics = this::onOpenAnalytics, onOpenAbout = this::onOpenAbout, - onVerifyClicked = this::onVerifyClicked, onSecureBackupClicked = this::onSecureBackupClicked, onOpenDeveloperSettings = this::onOpenDeveloperSettings, onOpenAdvancedSettings = this::onOpenAdvancedSettings, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index f80ee11aba..20c28427a8 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -74,7 +74,7 @@ class PreferencesRootPresenter @Inject constructor( } // We should display the 'complete verification' option if the current session can be verified - val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false) + val canVerifyUserSession by sessionVerificationService.canVerifySessionFlow.collectAsState(false) val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator() @@ -102,8 +102,7 @@ class PreferencesRootPresenter @Inject constructor( myUser = matrixUser.value, version = versionFormatter.get(), deviceId = matrixClient.deviceId, - showCompleteVerification = showCompleteVerification, - showSecureBackup = !showCompleteVerification, + showSecureBackup = !canVerifyUserSession, showSecureBackupBadge = showSecureBackupIndicator, accountManagementUrl = accountManagementUrl.value, devicesManagementUrl = devicesManagementUrl.value, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index eabdc80da0..336690638d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -24,7 +24,6 @@ data class PreferencesRootState( val myUser: MatrixUser, val version: String, val deviceId: String?, - val showCompleteVerification: Boolean, val showSecureBackup: Boolean, val showSecureBackupBadge: Boolean, val accountManagementUrl: String?, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index b688a493b6..3373cb6ba0 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -27,7 +27,6 @@ fun aPreferencesRootState( myUser = myUser, version = "Version 1.1 (1)", deviceId = "ILAKNDNASDLK", - showCompleteVerification = true, showSecureBackup = true, showSecureBackupBadge = true, accountManagementUrl = "aUrl", diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 293889e868..21132df5c9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -30,6 +30,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.user.UserPreferences +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -51,7 +52,6 @@ import io.element.android.libraries.ui.strings.CommonStrings fun PreferencesRootView( state: PreferencesRootState, onBackPressed: () -> Unit, - onVerifyClicked: () -> Unit, onSecureBackupClicked: () -> Unit, onManageAccountClicked: (url: String) -> Unit, onOpenAnalytics: () -> Unit, @@ -81,13 +81,6 @@ fun PreferencesRootView( }, user = state.myUser, ) - if (state.showCompleteVerification) { - ListItem( - headlineContent = { Text(text = stringResource(CommonStrings.common_verify_device)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.CheckCircle())), - onClick = onVerifyClicked - ) - } if (state.showSecureBackup) { ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) }, @@ -95,8 +88,6 @@ fun PreferencesRootView( trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge }, onClick = onSecureBackupClicked, ) - } - if (state.showCompleteVerification || state.showSecureBackup) { HorizontalDivider() } if (state.accountManagementUrl != null) { @@ -222,6 +213,7 @@ internal fun PreferencesRootViewLightPreview(@PreviewParameter(MatrixUserProvide internal fun PreferencesRootViewDarkPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreviewDark { ContentToPreview(matrixUser) } +@ExcludeFromCoverage @Composable private fun ContentToPreview(matrixUser: MatrixUser) { PreferencesRootView( @@ -232,7 +224,6 @@ private fun ContentToPreview(matrixUser: MatrixUser) { onOpenDeveloperSettings = {}, onOpenAdvancedSettings = {}, onOpenAbout = {}, - onVerifyClicked = {}, onSecureBackupClicked = {}, onManageAccountClicked = {}, onOpenNotificationSettings = {}, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt index 2d590b90c1..9227eef364 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt @@ -22,7 +22,7 @@ import android.content.Context import coil.Coil import coil.annotation.ExperimentalCoilApi import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.ftue.api.state.FtueState +import io.element.android.features.ftue.api.state.FtueService import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.features.roomlist.api.migration.MigrationScreenStore import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -45,7 +45,7 @@ class DefaultClearCacheUseCase @Inject constructor( private val coroutineDispatchers: CoroutineDispatchers, private val defaultCacheIndexProvider: DefaultCacheService, private val okHttpClient: Provider, - private val ftueState: FtueState, + private val ftueService: FtueService, private val migrationScreenStore: MigrationScreenStore, ) : ClearCacheUseCase { override suspend fun invoke() = withContext(coroutineDispatchers.io) { @@ -61,7 +61,7 @@ class DefaultClearCacheUseCase @Inject constructor( // Clear app cache context.cacheDir.deleteRecursively() // Clear some settings - ftueState.reset() + ftueService.reset() // Clear migration screen store migrationScreenStore.reset() // Ensure the app is restarted diff --git a/features/preferences/impl/src/main/res/values-be/translations.xml b/features/preferences/impl/src/main/res/values-be/translations.xml index ce931f9c5f..450a47ce61 100644 --- a/features/preferences/impl/src/main/res/values-be/translations.xml +++ b/features/preferences/impl/src/main/res/values-be/translations.xml @@ -1,6 +1,6 @@ - "Рэжым распрацоўніка" + "Рэжым распрацоўшчыка" "Падайце распрацоўнікам доступ да функцый і функцыянальным магчымасцям." "Базавы URL сервера званкоў Element" "Задайце свой сервер Element Call." @@ -10,7 +10,7 @@ "Калі выключыць, вашы пасведчанні аб прачытанні нікому не будуць адпраўляцца. Вы па-ранейшаму будзеце атрымліваць пасведчанні аб прачытанні ад іншых карыстальнікаў." "Падзяліцеся прысутнасцю" "Калі гэта выключана, вы не зможаце адпраўляць або атрымліваць апавяшчэнні аб прачытанні або апавяшчэнні аб наборы тэксту" - "Уключыце опцыю для прагляду крыніцы паведамлення на часовай шкале." + "Уключыце опцыю для прагляду паведамленняў у хроніцы." "У вас няма заблакіраваных карыстальнікаў" "Разблакіраваць" "Вы зноў зможаце ўбачыць усе паведамленні." @@ -44,9 +44,11 @@ "Усе" "Згадванні" "Апавясціць мяне" - "Апавясціць мяне ў @room" + "Апавясціць пра @room" "Каб атрымліваць апавяшчэнні, змяніце свой %1$s." "налады сістэмы" "Сістэмныя апавяшчэнні выключаны" "Апавяшчэнні" + "Выпраўленне непаладак" + "Выпраўленне непаладак з апавяшчэннямі" diff --git a/features/preferences/impl/src/main/res/values-cs/translations.xml b/features/preferences/impl/src/main/res/values-cs/translations.xml index ed209124ec..60bc4b2b3e 100644 --- a/features/preferences/impl/src/main/res/values-cs/translations.xml +++ b/features/preferences/impl/src/main/res/values-cs/translations.xml @@ -51,4 +51,6 @@ Pokud budete pokračovat, některá nastavení se mohou změnit." "systémová nastavení" "Systémová oznámení byla vypnuta" "Oznámení" + "Odstraňování problémů" + "Odstraňování problémů s upozorněními" diff --git a/features/preferences/impl/src/main/res/values-de/translations.xml b/features/preferences/impl/src/main/res/values-de/translations.xml index 6d47e4817d..65b4923452 100644 --- a/features/preferences/impl/src/main/res/values-de/translations.xml +++ b/features/preferences/impl/src/main/res/values-de/translations.xml @@ -49,4 +49,6 @@ Wenn du fortfährst, können sich einige deiner Einstellungen ändern." "Systemeinstellungen" "Systembenachrichtigungen deaktiviert" "Benachrichtigungen" + "Fehlerbehebung" + "Fehlerbehebung für Benachrichtigungen" diff --git a/features/preferences/impl/src/main/res/values-fr/translations.xml b/features/preferences/impl/src/main/res/values-fr/translations.xml index ddfd6eec5c..9c415fea2d 100644 --- a/features/preferences/impl/src/main/res/values-fr/translations.xml +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -49,4 +49,6 @@ Si vous continuez, il est possible que certains de vos paramètres soient modifi "paramètres du système" "Les notifications du système sont désactivées" "Notifications" + "Dépannage" + "Résoudre les problèmes liés aux notifications" diff --git a/features/preferences/impl/src/main/res/values-hu/translations.xml b/features/preferences/impl/src/main/res/values-hu/translations.xml index f57308e21d..841695edcf 100644 --- a/features/preferences/impl/src/main/res/values-hu/translations.xml +++ b/features/preferences/impl/src/main/res/values-hu/translations.xml @@ -1,16 +1,16 @@ "Fejlesztői mód" - "Engedélyezd, hogy elérd a fejlesztőknek szánt funkciókat." + "Engedélyezze, hogy elérje a fejlesztőknek szánt funkciókat." "Egyéni Element Call alapwebcím" "Egyéni alapwebcím beállítása az Element Callhoz." - "Érvénytelen webcím, győződj meg arról, hogy szerepel-e benne a protokoll (http/https), és hogy helyes-e a cím." - "A formázott szöveges szerkesztő letiltása, hogy kézzel írhass Markdownt." + "Érvénytelen webcím, győződjön meg arról, hogy szerepel-e benne a protokoll (http/https), és hogy helyes-e a cím." + "A formázott szöveges szerkesztő letiltása, hogy kézzel írhasson Markdownt." "Olvasási visszaigazolások" "Ha ki van kapcsolva, az olvasási visszaigazolások nem lesznek elküldve senkinek. A többi felhasználó olvasási visszaigazolását továbbra is meg fogja kapni." "Jelenlét megosztása" "Ha ki van kapcsolva, nem tud olvasási visszaigazolást vagy írási értesítést küldeni és fogadni" - "Engedélyezd a beállítást az üzenet forrásának megjelenítéséhez az idővonalon." + "Engedélyezze a beállítást az üzenet forrásának megjelenítéséhez az idővonalon." "Nincsenek letiltott felhasználók" "Letiltás feloldása" "Újra láthatja az összes üzenetét." @@ -49,4 +49,6 @@ Ha folytatja, egyes beállítások megváltozhatnak." "rendszerbeállításokat" "A rendszerértesítések ki vannak kapcsolva" "Értesítések" + "Hibaelhárítás" + "Értesítések hibaelhárítása" diff --git a/features/preferences/impl/src/main/res/values-in/translations.xml b/features/preferences/impl/src/main/res/values-in/translations.xml index f9fb4c0848..8bca603eeb 100644 --- a/features/preferences/impl/src/main/res/values-in/translations.xml +++ b/features/preferences/impl/src/main/res/values-in/translations.xml @@ -51,4 +51,6 @@ Jika Anda melanjutkan, beberapa pengaturan Anda dapat berubah." "pengaturan sistem" "Pemberitahuan sistem dimatikan" "Notifikasi" + "Pemecahan masalah" + "Pecahkan masalah notifikasi" diff --git a/features/preferences/impl/src/main/res/values-ru/translations.xml b/features/preferences/impl/src/main/res/values-ru/translations.xml index 06b2cddad5..3b24cf8c80 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -49,4 +49,6 @@ "настройки системы" "Системные уведомления выключены" "Уведомления" + "Устранение неполадок" + "Уведомления об устранении неполадок" diff --git a/features/preferences/impl/src/main/res/values-sk/translations.xml b/features/preferences/impl/src/main/res/values-sk/translations.xml index ddc71f8c67..f382d0ab6f 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -51,4 +51,6 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""nastavenia systému" "Systémové oznámenia sú vypnuté" "Oznámenia" + "Riešenie problémov" + "Oznámenia riešení problémov" diff --git a/features/preferences/impl/src/main/res/values-sv/translations.xml b/features/preferences/impl/src/main/res/values-sv/translations.xml index 7bcf1c81f0..aa4eea5a2e 100644 --- a/features/preferences/impl/src/main/res/values-sv/translations.xml +++ b/features/preferences/impl/src/main/res/values-sv/translations.xml @@ -2,6 +2,9 @@ "Utvecklarläge" "Aktivera för att ha tillgång till funktionalitet för utvecklare." + "Anpassad bas-URL för Element Call" + "Ange en anpassad bas-URL för Element Call." + "Ogiltig URL, se till att du inkluderar protokollet (http/https) och rätt adress." "Inaktivera rik-text-redigeraren för att skriva Markdown manuellt." "Avblockera" "Du kommer att kunna se alla meddelanden från dem igen." diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index 492c75295a..56a5c0ba03 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -49,4 +49,6 @@ If you proceed, some of your settings may change." "system settings" "System notifications turned off" "Notifications" + "Troubleshoot" + "Troubleshoot notifications" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt new file mode 100644 index 0000000000..8397d5aef6 --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.preferences.impl.notifications + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.preferences.impl.R +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class NotificationSettingsViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + eventSink = eventsRecorder + ), + onBackPressed = it + ) + rule.pressBack() + } + eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on troubleshoot notification invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + eventSink = eventsRecorder + ), + onTroubleshootNotificationsClicked = it + ) + rule.clickOn(R.string.troubleshoot_notifications_entry_point_title) + } + eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on group chats invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnceWithParam(false) { + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + eventSink = eventsRecorder + ), + onOpenEditDefault = it + ) + rule.clickOn(R.string.screen_notification_settings_group_chats) + } + eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on direct chats invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnceWithParam(true) { + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + eventSink = eventsRecorder + ), + onOpenEditDefault = it + ) + rule.clickOn(R.string.screen_notification_settings_direct_chats) + } + eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on disable notifications emits the expected events`() { + testNotificationToggle(true) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on enable notifications emits the expected events`() { + testNotificationToggle(false) + } + + private fun testNotificationToggle(initialState: Boolean) { + val eventsRecorder = EventsRecorder() + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + appNotificationEnabled = initialState, + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_notification_settings_enable_notifications) + eventsRecorder.assertList( + listOf( + NotificationSettingsEvents.RefreshSystemNotificationsEnabled, + NotificationSettingsEvents.SetNotificationsEnabled(!initialState) + ) + ) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on disable notify me on at room emits the expected events`() { + testAtRoomToggle(true) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on enable notify me on at room emits the expected events`() { + testAtRoomToggle(false) + } + + private fun testAtRoomToggle(initialState: Boolean) { + val eventsRecorder = EventsRecorder() + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + atRoomNotificationsEnabled = initialState, + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_notification_settings_room_mention_label) + eventsRecorder.assertList( + listOf( + NotificationSettingsEvents.RefreshSystemNotificationsEnabled, + NotificationSettingsEvents.SetAtRoomNotificationsEnabled(!initialState) + ) + ) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on disable notify me on invitation emits the expected events`() { + testInvitationToggle(true) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on enable notify me on invitation emits the expected events`() { + testInvitationToggle(false) + } + + private fun testInvitationToggle(initialState: Boolean) { + val eventsRecorder = EventsRecorder() + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + inviteForMeNotificationsEnabled = initialState, + eventSink = eventsRecorder + ), + ) + rule.clickOn(R.string.screen_notification_settings_invite_for_me_label) + eventsRecorder.assertList( + listOf( + NotificationSettingsEvents.RefreshSystemNotificationsEnabled, + NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(!initialState) + ) + ) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `with an error configuration, clicking on continue emits the expected events`() { + val eventsRecorder = EventsRecorder() + rule.setNotificationSettingsView( + state = aValidNotificationSettingsState( + changeNotificationSettingAction = AsyncAction.Failure(AN_EXCEPTION), + eventSink = eventsRecorder + ), + ) + rule.clickOn(CommonStrings.action_ok) + eventsRecorder.assertList( + listOf( + NotificationSettingsEvents.RefreshSystemNotificationsEnabled, + NotificationSettingsEvents.ClearNotificationChangeError + ) + ) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `with invalid configuration, clicking on continue emits the expected events`() { + val eventsRecorder = EventsRecorder() + rule.setNotificationSettingsView( + state = aInvalidNotificationSettingsState( + fixFailed = false, + eventSink = eventsRecorder + ), + ) + rule.clickOn(CommonStrings.action_continue) + eventsRecorder.assertList( + listOf( + NotificationSettingsEvents.RefreshSystemNotificationsEnabled, + NotificationSettingsEvents.FixConfigurationMismatch + ) + ) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `with invalid configuration and error, clicking on OK emits the expected events`() { + val eventsRecorder = EventsRecorder() + rule.setNotificationSettingsView( + state = aInvalidNotificationSettingsState( + fixFailed = true, + eventSink = eventsRecorder + ), + ) + rule.clickOn(CommonStrings.action_ok) + eventsRecorder.assertList( + listOf( + NotificationSettingsEvents.RefreshSystemNotificationsEnabled, + NotificationSettingsEvents.ClearConfigurationMismatchError + ) + ) + } +} + +private fun AndroidComposeTestRule.setNotificationSettingsView( + state: NotificationSettingsState, + onOpenEditDefault: (isOneToOne: Boolean) -> Unit = EnsureNeverCalledWithParam(), + onTroubleshootNotificationsClicked: () -> Unit = EnsureNeverCalled(), + onBackPressed: () -> Unit = EnsureNeverCalled(), +) { + setContent { + NotificationSettingsView( + state = state, + onOpenEditDefault = onOpenEditDefault, + onTroubleshootNotificationsClicked = onTroubleshootNotificationsClicked, + onBackPressed = onBackPressed, + ) + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 3288e94eb4..8bc6e92328 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -92,7 +92,6 @@ class PreferencesRootPresenterTest { ) ) assertThat(initialState.version).isEqualTo("A Version") - assertThat(loadedState.showCompleteVerification).isTrue() assertThat(loadedState.showSecureBackup).isFalse() assertThat(loadedState.showSecureBackupBadge).isTrue() assertThat(loadedState.accountManagementUrl).isNull() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/VersionFormatterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/VersionFormatterTest.kt index ebab2960e9..e41895440c 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/VersionFormatterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/VersionFormatterTest.kt @@ -23,25 +23,31 @@ import kotlinx.coroutines.test.runTest import org.junit.Test class VersionFormatterTest { - @Test - fun `version formatter should return simplified version for other branch`() = runTest { - val sut = DefaultVersionFormatter( - stringProvider = FakeStringProvider(defaultResult = VERSION), - buildMeta = aBuildMeta(gitBranchName = "main") - ) - assertThat(sut.get()).isEqualTo(VERSION) - } - @Test fun `version formatter should return simplified version for main branch`() = runTest { val sut = DefaultVersionFormatter( stringProvider = FakeStringProvider(defaultResult = VERSION), buildMeta = aBuildMeta( + gitBranchName = "main", + versionName = "versionName", + versionCode = 123 + ) + ) + assertThat(sut.get()).isEqualTo("${VERSION}versionName, 123") + } + + @Test + fun `version formatter should return simplified version for other branch`() = runTest { + val sut = DefaultVersionFormatter( + stringProvider = FakeStringProvider(defaultResult = VERSION), + buildMeta = aBuildMeta( + versionName = "versionName", + versionCode = 123, gitBranchName = "branch", gitRevision = "1234567890", ) ) - assertThat(sut.get()).isEqualTo("$VERSION\nbranch (1234567890)") + assertThat(sut.get()).isEqualTo("${VERSION}versionName, 123\nbranch (1234567890)") } companion object { diff --git a/features/rageshake/api/src/main/res/values-be/translations.xml b/features/rageshake/api/src/main/res/values-be/translations.xml index a73c7eedd5..3d9eec29d2 100644 --- a/features/rageshake/api/src/main/res/values-be/translations.xml +++ b/features/rageshake/api/src/main/res/values-be/translations.xml @@ -1,7 +1,7 @@ - "Пры апошнім выкарыстанні %1$s адбыўся збой. Жадаеце падзяліцца справаздачай аб збоі?" - "Падобна, што вы трасеце тэлефон. Жадаеце адкрыць экран паведамлення пра памылку?" + "Пры апошнім выкарыстанні %1$s адбыўся збой. Хочаце падзяліцца справаздачай аб збоі?" + "Падобна, што вы трасеце тэлефон. Хочаце адкрыць экран паведамлення пра памылку?" "Rageshake" "Парог выяўлення" diff --git a/features/rageshake/api/src/main/res/values-hu/translations.xml b/features/rageshake/api/src/main/res/values-hu/translations.xml index 5b228df7a0..e2fc862b9d 100644 --- a/features/rageshake/api/src/main/res/values-hu/translations.xml +++ b/features/rageshake/api/src/main/res/values-hu/translations.xml @@ -1,7 +1,7 @@ - "Az %1$s összeomlott a legutóbbi használata óta. Megosztod velünk az összeomlás-jelentést?" - "Úgy tűnik, mintha dühösen ráznád a telefont. Megnyitod a hibajelentési képernyőt?" + "Az %1$s összeomlott a legutóbbi használata óta. Megosztja velünk az összeomlás-jelentést?" + "Úgy tűnik, mintha dühösen rázná a telefont. Megnyitja a hibajelentési képernyőt?" "Ideges rázás" "Észlelési küszöb" diff --git a/features/rageshake/impl/src/main/res/values-be/translations.xml b/features/rageshake/impl/src/main/res/values-be/translations.xml index 277ba258be..f927ba3c5e 100644 --- a/features/rageshake/impl/src/main/res/values-be/translations.xml +++ b/features/rageshake/impl/src/main/res/values-be/translations.xml @@ -8,10 +8,10 @@ "Апішыце праблему…" "Калі магчыма, калі ласка, напішыце апісанне на англійскай мове." "Апісанне занадта кароткае. Дайце больш падрабязную інфармацыю аб тым, што адбылося. Дзякуй!" - "Адправіць часопісы збояў" - "Дазволіць часопісы" + "Адправіць журналы збояў" + "Дазволіць журналы" "Адправіць здымак экрана" "Каб пераканацца, што ўсё працуе правільна, у паведамленне будуць уключаны часопісы. Каб адправіць паведамленне без часопісаў, адключыце гэтую наладу." - "Пры апошнім выкарыстанні %1$s адбыўся збой. Жадаеце падзяліцца справаздачай аб збоі?" - "Прагляд часопісаў" + "Пры апошнім выкарыстанні %1$s адбыўся збой. Хочаце падзяліцца справаздачай аб збоі?" + "Прагляд журналаў" diff --git a/features/rageshake/impl/src/main/res/values-hu/translations.xml b/features/rageshake/impl/src/main/res/values-hu/translations.xml index 616fd1d6cc..1ac8c4c285 100644 --- a/features/rageshake/impl/src/main/res/values-hu/translations.xml +++ b/features/rageshake/impl/src/main/res/values-hu/translations.xml @@ -12,6 +12,6 @@ "Naplók engedélyezése" "Képernyőkép küldése" "A naplók szerepelni fognak az üzenetben, hogy megbizonyosodhassunk arról, hogy minden megfelelően működik-e. Ha naplók nélkül szeretné elküldeni az üzenetet, akkor kapcsolja ki ezt a beállítást." - "Az %1$s összeomlott a legutóbbi használata óta. Megosztod velünk az összeomlás-jelentést?" + "Az %1$s összeomlott a legutóbbi használata óta. Megosztja velünk az összeomlás-jelentést?" "Naplók megtekintése" diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 63cc745ecc..6bf8b00dd7 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -45,6 +45,7 @@ class RoomDetailsNode @AssistedInject constructor( private val presenter: RoomDetailsPresenter, private val room: MatrixRoom, private val analyticsService: AnalyticsService, + private val permalinkBuilder: PermalinkBuilder, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { fun openRoomMemberList() @@ -84,8 +85,8 @@ class RoomDetailsNode @AssistedInject constructor( private fun onShareRoom(context: Context) { val alias = room.alias ?: room.alternativeAliases.firstOrNull() - val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) } - ?: PermalinkBuilder.permalinkForRoomId(room.roomId) + val permalinkResult = alias?.let { permalinkBuilder.permalinkForRoomAlias(it) } + ?: permalinkBuilder.permalinkForRoomId(room.roomId) permalinkResult.onSuccess { permalink -> context.startSharePlainTextIntent( activityResultLauncher = null, @@ -99,7 +100,7 @@ class RoomDetailsNode @AssistedInject constructor( } private fun onShareMember(context: Context, member: RoomMember) { - val permalinkResult = PermalinkBuilder.permalinkForUser(member.userId) + val permalinkResult = permalinkBuilder.permalinkForUser(member.userId) permalinkResult.onSuccess { permalink -> context.startSharePlainTextIntent( activityResultLauncher = null, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 9e4030201a..f0c96e74e2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -78,10 +78,6 @@ class RoomDetailsPresenter @Inject constructor( val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } } val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } } - val isRoomModerationEnabled by produceState(initialValue = false) { - value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) - } - LaunchedEffect(Unit) { canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) if (canShowNotificationSettings.value) { @@ -147,7 +143,7 @@ class RoomDetailsPresenter @Inject constructor( leaveRoomState = leaveRoomState, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), isFavorite = isFavorite, - displayRolesAndPermissionsSettings = isRoomModerationEnabled && !room.isDm && isUserAdmin, + displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin, eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index c05388bfc7..305393c822 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -34,8 +34,6 @@ import io.element.android.features.roomdetails.impl.members.moderation.RoomMembe import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -50,7 +48,6 @@ class RoomMemberListPresenter @AssistedInject constructor( private val room: MatrixRoom, private val roomMemberListDataSource: RoomMemberListDataSource, private val coroutineDispatchers: CoroutineDispatchers, - private val featureFlagService: FeatureFlagService, private val roomMembersModerationPresenter: RoomMembersModerationPresenter, @Assisted private val navigator: RoomMemberListNavigator, ) : Presenter { @@ -74,15 +71,7 @@ class RoomMemberListPresenter @AssistedInject constructor( value = room.canInvite().getOrElse { false } } - val isRoomModerationEnabled by produceState(initialValue = false) { - value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) - } - - val roomModerationState = if (isRoomModerationEnabled) { - roomMembersModerationPresenter.present() - } else { - remember { roomMembersModerationPresenter.dummyState() } - } + val roomModerationState = roomMembersModerationPresenter.present() // Ensure we load the latest data when entering this screen LaunchedEffect(Unit) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index bdf7385661..71cd975e18 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -46,6 +46,7 @@ class RoomMemberDetailsNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val analyticsService: AnalyticsService, + private val permalinkBuilder: PermalinkBuilder, presenterFactory: RoomMemberDetailsPresenter.Factory, ) : Node(buildContext, plugins = plugins) { interface Callback : NodeInputs { @@ -74,7 +75,7 @@ class RoomMemberDetailsNode @AssistedInject constructor( val context = LocalContext.current fun onShareUser() { - val permalinkResult = PermalinkBuilder.permalinkForUser(inputs.roomMemberId) + val permalinkResult = permalinkBuilder.permalinkForUser(inputs.roomMemberId) permalinkResult.onSuccess { permalink -> context.startSharePlainTextIntent( activityResultLauncher = null, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt index 7c75a9e369..01f3ca2ea0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt @@ -33,6 +33,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.roomdetails.impl.R import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.button.BackButton @@ -127,6 +128,7 @@ internal fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetai internal fun RoomMemberDetailsViewDarkPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = ElementPreviewDark { ContentToPreview(state) } +@ExcludeFromCoverage @Composable private fun ContentToPreview(state: RoomMemberDetailsState) { RoomMemberDetailsView( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt index ad18c07eb4..f35aa1f211 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/DefaultRoomMembersModerationPresenter.kt @@ -31,8 +31,6 @@ import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.finally import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember @@ -51,7 +49,6 @@ import javax.inject.Inject @ContributesBinding(RoomScope::class) class DefaultRoomMembersModerationPresenter @Inject constructor( private val room: MatrixRoom, - private val featureFlagService: FeatureFlagService, private val dispatchers: CoroutineDispatchers, private val analyticsService: AnalyticsService, ) : RoomMembersModerationPresenter { @@ -61,9 +58,8 @@ class DefaultRoomMembersModerationPresenter @Inject constructor( private suspend fun canKick() = room.canKick().getOrDefault(false) override suspend fun canDisplayModerationActions(): Boolean { - val isRoomModerationEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) val isDm = room.isDm && room.isEncrypted - return isRoomModerationEnabled && !isDm && (canBan() || canKick()) + return !isDm && (canBan() || canKick()) } @Composable @@ -76,7 +72,7 @@ class DefaultRoomMembersModerationPresenter @Inject constructor( val unbanUserAsyncAction = remember { mutableStateOf(AsyncAction.Uninitialized as AsyncAction) } val canDisplayBannedUsers by produceState(initialValue = false) { - value = featureFlagService.isFeatureEnabled(FeatureFlags.RoomModeration) && !room.isDm && canBan() + value = !room.isDm && canBan() } fun handleEvent(event: RoomMembersModerationEvents) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt index 659dc6c255..3f7224602d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/RolesAndPermissionsPresenter.kt @@ -29,9 +29,11 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.joinedRoomMembers import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.CoroutineScope @@ -47,14 +49,22 @@ class RolesAndPermissionsPresenter @Inject constructor( override fun present(): RolesAndPermissionsState { val coroutineScope = rememberCoroutineScope() val roomInfo by room.roomInfoFlow.collectAsState(initial = null) + val roomMembers by room.membersStateFlow.collectAsState() + // Get the list of joined room members, in order to filter members present in the power + // level state Event, but not member of the room anymore. + val joinedRoomMemberIds by remember { + derivedStateOf { + roomMembers.joinedRoomMembers().map { it.userId } + } + } val moderatorCount by remember { derivedStateOf { - roomInfo.userCountWithRole(RoomMember.Role.MODERATOR) + roomInfo.userCountWithRole(joinedRoomMemberIds, RoomMember.Role.MODERATOR) } } val adminCount by remember { derivedStateOf { - roomInfo.userCountWithRole(RoomMember.Role.ADMIN) + roomInfo.userCountWithRole(joinedRoomMemberIds, RoomMember.Role.ADMIN) } } val changeOwnRoleAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } @@ -108,11 +118,9 @@ class RolesAndPermissionsPresenter @Inject constructor( } } - private fun MatrixRoomInfo?.userCountWithRole(role: RoomMember.Role): Int { - return if (this != null) { - userPowerLevels.count { (_, level) -> RoomMember.Role.forPowerLevel(level) == role } - } else { - 0 + private fun MatrixRoomInfo?.userCountWithRole(joinedRoomMemberIds: List, role: RoomMember.Role): Int { + return this?.userPowerLevels.orEmpty().count { (userId, level) -> + RoomMember.Role.forPowerLevel(level) == role && userId in joinedRoomMemberIds } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesEvent.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesEvent.kt index afb12a2e50..4176f5f9ce 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesEvent.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesEvent.kt @@ -16,12 +16,12 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles -import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.user.MatrixUser sealed interface ChangeRolesEvent { data object ToggleSearchActive : ChangeRolesEvent data class QueryChanged(val query: String?) : ChangeRolesEvent - data class UserSelectionToggled(val roomMember: RoomMember) : ChangeRolesEvent + data class UserSelectionToggled(val matrixUser: MatrixUser) : ChangeRolesEvent data object Save : ChangeRolesEvent data object Exit : ChangeRolesEvent data object CancelExit : ChangeRolesEvent diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesNode.kt index 291beebe99..acd0d7c20c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesNode.kt @@ -65,7 +65,7 @@ class ChangeRolesNode @AssistedInject constructor( ChangeRolesView( modifier = modifier, state = state, - onBackPressed = this::navigateUp, + navigateUp = this::navigateUp, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt index dc1c2ef70e..b4522367ec 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesPresenter.kt @@ -42,6 +42,8 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange +import io.element.android.libraries.matrix.api.room.powerlevels.usersWithRole +import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList @@ -73,7 +75,7 @@ class ChangeRolesPresenter @AssistedInject constructor( var query by rememberSaveable { mutableStateOf(null) } var searchActive by rememberSaveable { mutableStateOf(false) } var searchResults by remember { - mutableStateOf>>(SearchBarResultState.Initial()) + mutableStateOf>(SearchBarResultState.Initial()) } val selectedUsers = remember { mutableStateOf>(persistentListOf()) @@ -89,7 +91,7 @@ class ChangeRolesPresenter @AssistedInject constructor( // Users who were selected but didn't have the role, so their role change was pending val toAdd = selectedUsers.value.filter { user -> users.none { it.userId == user.userId } && previous.none { it.userId == user.userId } } // Users who no longer have the role - val toRemove = previous.filter { user -> users.none { it.userId == user.userId } } + val toRemove = previous.filter { user -> users.none { it.userId == user.userId } }.toSet() selectedUsers.value = (users + toAdd - toRemove).toImmutableList() } .launchIn(this) @@ -101,8 +103,9 @@ class ChangeRolesPresenter @AssistedInject constructor( LaunchedEffect(query, roomMemberState) { val results = dataSource .search(query.orEmpty()) - .sorted() + .groupedByRole() + println(results) searchResults = if (results.isEmpty()) { SearchBarResultState.NoResultsFound() } else { @@ -129,11 +132,11 @@ class ChangeRolesPresenter @AssistedInject constructor( } is ChangeRolesEvent.UserSelectionToggled -> { val newList = selectedUsers.value.toMutableList() - val index = newList.indexOfFirst { it.userId == event.roomMember.userId } + val index = newList.indexOfFirst { it.userId == event.matrixUser.userId } if (index >= 0) { newList.removeAt(index) } else { - newList.add(event.roomMember.toMatrixUser()) + newList.add(event.matrixUser) } selectedUsers.value = newList.toImmutableList() } @@ -179,16 +182,18 @@ class ChangeRolesPresenter @AssistedInject constructor( ) } + private fun List.groupedByRole(): MembersByRole { + return MembersByRole( + admins = filter { it.role == RoomMember.Role.ADMIN }.sorted(), + moderators = filter { it.role == RoomMember.Role.MODERATOR }.sorted(), + members = filter { it.role == RoomMember.Role.USER }.sorted(), + ) + } + private fun Iterable.sorted(): ImmutableList { return sortedWith(PowerLevelRoomMemberComparator()).toImmutableList() } - private fun RoomMember.toMatrixUser() = MatrixUser( - userId = userId, - displayName = displayName, - avatarUrl = avatarUrl, - ) - private fun CoroutineScope.save( usersWithRole: ImmutableList, selectedUsers: MutableState>, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesState.kt index 79a955df13..973363ae6a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesState.kt @@ -16,18 +16,20 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles +import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList data class ChangeRolesState( val role: RoomMember.Role, val query: String?, val isSearchActive: Boolean, - val searchResults: SearchBarResultState>, + val searchResults: SearchBarResultState, val selectedUsers: ImmutableList, val hasPendingChanges: Boolean, val exitState: AsyncAction, @@ -35,3 +37,21 @@ data class ChangeRolesState( val canChangeMemberRole: (UserId) -> Boolean, val eventSink: (ChangeRolesEvent) -> Unit, ) + +data class MembersByRole( + val admins: ImmutableList, + val moderators: ImmutableList, + val members: ImmutableList, +) { + constructor(members: List) : this( + admins = members.filter { it.role == RoomMember.Role.ADMIN }.sorted(), + moderators = members.filter { it.role == RoomMember.Role.MODERATOR }.sorted(), + members = members.filter { it.role == RoomMember.Role.USER }.sorted(), + ) + + fun isEmpty() = admins.isEmpty() && moderators.isEmpty() && members.isEmpty() +} + +private fun Iterable.sorted(): ImmutableList { + return sortedWith(PowerLevelRoomMemberComparator()).toImmutableList() +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesStateProvider.kt index 69e8d72be6..c0e1a2c135 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesStateProvider.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import kotlinx.collections.immutable.ImmutableList @@ -32,7 +33,7 @@ class ChangeRolesStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aChangeRolesState(), - aChangeRolesState(role = RoomMember.Role.MODERATOR), + aChangeRolesStateWithSelectedUsers().copy(role = RoomMember.Role.MODERATOR), aChangeRolesStateWithSelectedUsers().copy(hasPendingChanges = false), aChangeRolesStateWithSelectedUsers(), aChangeRolesStateWithSelectedUsers().copy( @@ -41,7 +42,7 @@ class ChangeRolesStateProvider : PreviewParameterProvider { aChangeRolesStateWithSelectedUsers().copy( query = "Alice", isSearchActive = true, - searchResults = SearchBarResultState.Results(aRoomMemberList().take(1).toImmutableList()), + searchResults = SearchBarResultState.Results(MembersByRole(aRoomMemberList().take(1).toImmutableList())), selectedUsers = aMatrixUserList().take(1).toImmutableList(), ), aChangeRolesStateWithSelectedUsers().copy(exitState = AsyncAction.Confirming), @@ -56,12 +57,13 @@ internal fun aChangeRolesState( role: RoomMember.Role = RoomMember.Role.ADMIN, query: String? = null, isSearchActive: Boolean = false, - searchResults: SearchBarResultState> = SearchBarResultState.NoResultsFound(), + searchResults: SearchBarResultState = SearchBarResultState.NoResultsFound(), selectedUsers: ImmutableList = persistentListOf(), hasPendingChanges: Boolean = false, exitState: AsyncAction = AsyncAction.Uninitialized, savingState: AsyncAction = AsyncAction.Uninitialized, canRemoveMember: (UserId) -> Boolean = { true }, + eventSink: (ChangeRolesEvent) -> Unit = {}, ) = ChangeRolesState( role = role, query = query, @@ -72,12 +74,22 @@ internal fun aChangeRolesState( exitState = exitState, savingState = savingState, canChangeMemberRole = canRemoveMember, - eventSink = {}, + eventSink = eventSink, ) internal fun aChangeRolesStateWithSelectedUsers() = aChangeRolesState( selectedUsers = aMatrixUserList().toImmutableList(), - searchResults = SearchBarResultState.Results(aRoomMemberList().toImmutableList()), + searchResults = SearchBarResultState.Results( + MembersByRole( + members = aRoomMemberList().mapIndexed { index, roomMember -> + if (index % 2 == 0) { + roomMember.copy(membership = RoomMembershipState.INVITE) + } else { + roomMember + } + } + ) + ), hasPendingChanges = true, canRemoveMember = { it != UserId("@alice:server.org") }, ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt index 9510ca4272..450005ae4a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt @@ -26,8 +26,10 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.systemBarsPadding @@ -36,23 +38,29 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.roomdetails.impl.R -import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog @@ -68,27 +76,24 @@ import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.getBestName +import io.element.android.libraries.matrix.api.room.toMatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.matrix.ui.components.MatrixUserRow import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf @OptIn(ExperimentalMaterial3Api::class) @Composable fun ChangeRolesView( state: ChangeRolesState, - onBackPressed: () -> Unit, + navigateUp: () -> Unit, modifier: Modifier = Modifier, ) { - val updatedOnBackPressed by rememberUpdatedState(newValue = onBackPressed) - BackHandler { - if (state.isSearchActive) { - state.eventSink(ChangeRolesEvent.ToggleSearchActive) - } else { - state.eventSink(ChangeRolesEvent.Exit) - } + val updatedNavigateUp by rememberUpdatedState(newValue = navigateUp) + BackHandler(enabled = !state.isSearchActive) { + state.eventSink(ChangeRolesEvent.Exit) } Box(modifier = modifier) { @@ -129,7 +134,9 @@ fun ChangeRolesView( ) { val lazyListState = rememberLazyListState() SearchBar( - modifier = Modifier.padding(bottom = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), placeHolderTitle = stringResource(CommonStrings.common_search_for_someone), query = state.query.orEmpty(), onQueryChange = { state.eventSink(ChangeRolesEvent.QueryChanged(it)) }, @@ -138,12 +145,12 @@ fun ChangeRolesView( resultState = state.searchResults, ) { members -> SearchResultsList( - isSearchActive = true, + currentRole = state.role, lazyListState = lazyListState, searchResults = members, selectedUsers = state.selectedUsers, canRemoveMember = state.canChangeMemberRole, - onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) }, + onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it.toMatrixUser())) }, selectedUsersList = {}, ) } @@ -154,18 +161,18 @@ fun ChangeRolesView( ) { Column { SearchResultsList( - isSearchActive = false, + currentRole = state.role, lazyListState = lazyListState, - searchResults = (state.searchResults as? SearchBarResultState.Results)?.results ?: persistentListOf(), + searchResults = (state.searchResults as? SearchBarResultState.Results)?.results ?: MembersByRole(emptyList()), selectedUsers = state.selectedUsers, canRemoveMember = state.canChangeMemberRole, - onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) }, + onSelectionToggled = { state.eventSink(ChangeRolesEvent.UserSelectionToggled(it.toMatrixUser())) }, selectedUsersList = { users -> SelectedUsersRowList( contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp), selectedUsers = users, onUserRemoved = { - state.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(it.userId))) + state.eventSink(ChangeRolesEvent.UserSelectionToggled(it)) }, canDeselect = { state.canChangeMemberRole(it.userId) }, ) @@ -181,7 +188,7 @@ fun ChangeRolesView( AsyncActionView( async = state.exitState, - onSuccess = { updatedOnBackPressed() }, + onSuccess = { updatedNavigateUp() }, confirmationDialog = { ConfirmationDialog( title = stringResource(CommonStrings.dialog_unsaved_changes_title), @@ -229,8 +236,8 @@ fun ChangeRolesView( @OptIn(ExperimentalFoundationApi::class) @Composable private fun SearchResultsList( - isSearchActive: Boolean, - searchResults: ImmutableList, + currentRole: RoomMember.Role, + searchResults: MembersByRole, selectedUsers: ImmutableList, canRemoveMember: (UserId) -> Boolean, onSelectionToggled: (RoomMember) -> Unit, @@ -243,43 +250,145 @@ private fun SearchResultsList( item { selectedUsersList(selectedUsers) } - stickyHeader { - val textResId = if (isSearchActive) { - CommonStrings.common_search_results - } else { - R.string.screen_room_member_list_room_members_header_title - } - Text( - modifier = Modifier - .background(ElementTheme.colors.bgCanvasDefault) - .padding(horizontal = 16.dp, vertical = 8.dp) - .fillMaxWidth(), - text = stringResource(textResId), - style = ElementTheme.typography.fontBodyLgMedium, - ) - } - items(searchResults, key = { it.userId }) { roomMember -> - val canToggle = canRemoveMember(roomMember.userId) - val trailingContent: @Composable (() -> Unit)? = if (canToggle) { - { - Checkbox( - checked = selectedUsers.any { it.userId == roomMember.userId }, - onCheckedChange = { onSelectionToggled(roomMember) }, + if (searchResults.admins.isNotEmpty()) { + stickyHeader { ListSectionHeader(text = stringResource(R.string.screen_room_roles_and_permissions_admins)) } + // Add a footer for the admin section in change role to moderator screen + if (currentRole == RoomMember.Role.MODERATOR) { + item { + Text( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp), + text = stringResource(R.string.screen_room_change_role_moderators_admin_section_footer), + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodySmRegular, ) } - } else { - null } - MatrixUserRow( - modifier = Modifier.clickable(enabled = canToggle, onClick = { onSelectionToggled(roomMember) }), - matrixUser = MatrixUser( - userId = roomMember.userId, - displayName = roomMember.displayName, - avatarUrl = roomMember.avatarUrl, - ), - trailingContent = trailingContent, - ) + items(searchResults.admins, key = { it.userId }) { roomMember -> + ListMemberItem( + roomMember = roomMember, + canRemoveMember = canRemoveMember, + onSelectionToggled = onSelectionToggled, + selectedUsers = selectedUsers + ) + } } + if (searchResults.moderators.isNotEmpty()) { + stickyHeader { ListSectionHeader(text = stringResource(R.string.screen_room_roles_and_permissions_moderators)) } + items(searchResults.moderators, key = { it.userId }) { roomMember -> + ListMemberItem( + roomMember = roomMember, + canRemoveMember = canRemoveMember, + onSelectionToggled = onSelectionToggled, + selectedUsers = selectedUsers + ) + } + } + if (searchResults.members.isNotEmpty()) { + stickyHeader { ListSectionHeader(text = stringResource(R.string.screen_room_member_list_mode_members)) } + items(searchResults.members, key = { it.userId }) { roomMember -> + ListMemberItem( + roomMember = roomMember, + canRemoveMember = canRemoveMember, + onSelectionToggled = onSelectionToggled, + selectedUsers = selectedUsers + ) + } + } + } +} + +@Composable +private fun ListSectionHeader(text: String) { + Text( + modifier = Modifier + .background(ElementTheme.colors.bgCanvasDefault) + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + text = text, + style = ElementTheme.typography.fontBodyLgMedium, + ) +} + +@Composable +private fun ListMemberItem( + roomMember: RoomMember, + canRemoveMember: (UserId) -> Boolean, + onSelectionToggled: (RoomMember) -> Unit, + selectedUsers: ImmutableList, +) { + val canToggle = canRemoveMember(roomMember.userId) + val trailingContent: @Composable (() -> Unit) = { + Checkbox( + checked = selectedUsers.any { it.userId == roomMember.userId }, + onCheckedChange = { onSelectionToggled(roomMember) }, + enabled = canToggle, + ) + } + MemberRow( + modifier = Modifier.clickable(enabled = canToggle, onClick = { onSelectionToggled(roomMember) }), + avatarData = AvatarData(roomMember.userId.value, roomMember.displayName, roomMember.avatarUrl, AvatarSize.UserListItem), + name = roomMember.getBestName(), + userId = roomMember.userId.value.takeIf { roomMember.displayName?.isNotBlank() == true }, + isPending = roomMember.membership == RoomMembershipState.INVITE, + trailingContent = trailingContent, + ) +} + +@Composable +private fun MemberRow( + avatarData: AvatarData, + name: String, + userId: String?, + isPending: Boolean, + modifier: Modifier = Modifier, + trailingContent: @Composable (() -> Unit)? = null, +) { + Row( + modifier = modifier + .fillMaxWidth() + .heightIn(min = 56.dp) + .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Avatar(avatarData) + Column( + modifier = Modifier + .padding(start = 12.dp) + .weight(1f), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + // Name + Text( + modifier = Modifier.weight(1f, fill = false), + text = name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colorScheme.primary, + style = ElementTheme.typography.fontBodyLgRegular, + ) + // Invitation pending marker + if (isPending) { + Text( + modifier = Modifier.padding(start = 8.dp), + text = stringResource(id = R.string.screen_room_member_list_pending_header_title), + style = ElementTheme.typography.fontBodySmRegular.copy(fontStyle = FontStyle.Italic), + color = MaterialTheme.colorScheme.secondary + ) + } + } + // Id + userId?.let { + Text( + text = userId, + color = MaterialTheme.colorScheme.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = ElementTheme.typography.fontBodySmRegular, + ) + } + } + trailingContent?.invoke() } } @@ -289,7 +398,27 @@ internal fun ChangeRolesViewPreview(@PreviewParameter(ChangeRolesStateProvider:: ElementPreview { ChangeRolesView( state = state, - onBackPressed = {}, + navigateUp = {}, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun PendingMemberRowWithLongNamePreview() { + ElementPreview { + MemberRow( + avatarData = AvatarData("userId", "A very long name that should be truncated", "https://example.com/avatar.png", AvatarSize.UserListItem), + name = "A very long name that should be truncated", + userId = "@alice:matrix.org", + isPending = true, + trailingContent = { + Checkbox( + checked = true, + onCheckedChange = {}, + enabled = true, + ) + } ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt index 1d57545a0b..c9e09c7c68 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/permissions/ChangeRoomPermissionsView.kt @@ -17,8 +17,9 @@ package io.element.android.features.roomdetails.impl.rolesandpermissions.permissions import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -80,29 +81,35 @@ fun ChangeRoomPermissionsView( ) } ) { padding -> - Column(modifier = Modifier.padding(padding)) { + LazyColumn( + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) { for ((index, permissionItem) in state.items.withIndex()) { - ListSectionHeader(titleForSection(item = permissionItem), hasDivider = index > 0) - SelectRoleItem( - permissionsItem = permissionItem, - role = RoomMember.Role.ADMIN, - currentPermissions = state.currentPermissions - ) { item, role -> - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role)) - } - SelectRoleItem( - permissionsItem = permissionItem, - role = RoomMember.Role.MODERATOR, - currentPermissions = state.currentPermissions - ) { item, role -> - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role)) - } - SelectRoleItem( - permissionsItem = permissionItem, - role = RoomMember.Role.USER, - currentPermissions = state.currentPermissions - ) { item, role -> - state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role)) + item { + ListSectionHeader(titleForSection(item = permissionItem), hasDivider = index > 0) + SelectRoleItem( + permissionsItem = permissionItem, + role = RoomMember.Role.ADMIN, + currentPermissions = state.currentPermissions + ) { item, role -> + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role)) + } + SelectRoleItem( + permissionsItem = permissionItem, + role = RoomMember.Role.MODERATOR, + currentPermissions = state.currentPermissions + ) { item, role -> + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role)) + } + SelectRoleItem( + permissionsItem = permissionItem, + role = RoomMember.Role.USER, + currentPermissions = state.currentPermissions + ) { item, role -> + state.eventSink(ChangeRoomPermissionsEvent.ChangeMinimumRoleForAction(item, role)) + } } } } diff --git a/features/roomdetails/impl/src/main/res/values-be/translations.xml b/features/roomdetails/impl/src/main/res/values-be/translations.xml index 9419d246cc..be2ac558fd 100644 --- a/features/roomdetails/impl/src/main/res/values-be/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-be/translations.xml @@ -29,7 +29,12 @@ "Паніжэнне ўзроўню" "Вы не зможаце адмяніць гэтае змяненне, бо паніжаеце сябе. Калі вы апошні адміністратар у пакоі, вярнуць права будзе немагчыма." "Панізіць сябе?" + "%1$s (У чаканні)" + "(У чаканні)" "Рэдагаваць мадэратараў" + "Адміністратары" + "Мадэратары" + "Удзельнікі" "У вас ёсць незахаваныя змены." "Захаваць змены?" "Дадаць тэму" @@ -46,8 +51,8 @@ "Запрасіць карыстальникаў" "Пакінуць размову" "Пакінуць пакой" - "Карыстальніцкі" - "Па змаўчанні" + "Уласныя" + "Стандартныя" "Апавяшчэнні" "Ролі і дазволы" "Назва пакоя" @@ -62,7 +67,7 @@ "Блакіроўка %1$s" "%1$d карыстальнік" - "%1$d карыстальнікаў" + "%1$d карыстальніка" "%1$d карыстальнікаў" "Выдаліць і заблакіраваць удзельніка" @@ -83,14 +88,14 @@ "Удзельнікі пакоя" "Разблакіроўка %1$s" "Дазволіць карыстальніцкую наладу" - "Калі гэта ўключыць, ваша налада па змаўчанні будзе адменена" + "Калі гэта ўключыць, ваша налада прадвызначана будзе адменена" "Апавяшчаць мяне ў гэтым чаце для" - "Вы можаце змяніць яго ў сваім %1$s." - "глабальныя налады" - "Налада па змаўчанні" + "Вы можаце змяніць гэта ў %1$s." + "асноўных наладах" + "Стандартная налада" "Выдаліць карыстальніцкую наладу" "Падчас загрузкі налад апавяшчэнняў адбылася памылка." - "Не атрымалася аднавіць рэжым па змаўчанні, паспрабуйце яшчэ раз." + "Не атрымалася аднавіць прадвызначаны рэжым, паспрабуйце яшчэ раз." "Не ўдалося наладзіць рэжым, паспрабуйце яшчэ раз." "Ваш хатні сервер не падтрымлівае гэту опцыю ў зашыфраваных пакоях, вы не атрымаеце апавяшчэнне ў гэтым пакоі." "Усе паведамленні" diff --git a/features/roomdetails/impl/src/main/res/values-bg/translations.xml b/features/roomdetails/impl/src/main/res/values-bg/translations.xml index 80223c8cfa..5cc105f657 100644 --- a/features/roomdetails/impl/src/main/res/values-bg/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-bg/translations.xml @@ -5,6 +5,7 @@ "Отблокиране" "Отблокиране на потребителя" "Анкети" + "Членове" "Добавяне на тема" "Вече е член" "Вече е бил поканен" diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml index a45c7e25a1..93e7fcf389 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -29,7 +29,11 @@ "Degradovat" "Tuto změnu nebudete moci vrátit zpět, protože sami degradujete, pokud jste posledním privilegovaným uživatelem v místnosti, nebude možné znovu získat oprávnění." "Degradovat se?" + "%1$s (čekající)" "Upravit moderátory" + "Správci" + "Moderátoři" + "Členové" "Máte neuložené změny." "Uložit změny?" "Přidat téma" diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml index fe16998a72..980f149bad 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -29,7 +29,11 @@ "Zurückstufen" "Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Benutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen." "Möchtest Du Dich selbst herabstufen?" + "%1$s (Ausstehend)" "Moderatoren bearbeiten" + "Administratoren" + "Moderatoren" + "Mitglieder" "Du hast nicht gespeicherte Änderungen." "Änderungen speichern?" "Thema hinzufügen" @@ -76,7 +80,7 @@ "Gesperrt" "Mitglieder" "Ausstehend" - "%1$s wird entfernt" + "%1$s wird entfernt." "Administrator" "Moderator" "Raummitglieder" diff --git a/features/roomdetails/impl/src/main/res/values-fr/translations.xml b/features/roomdetails/impl/src/main/res/values-fr/translations.xml index c88514f68b..005531b3db 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -29,7 +29,12 @@ "Rétrograder" "Vous ne pourrez pas annuler ce changement car vous vous rétrogradez, si vous êtes le dernier utilisateur privilégié du salon il sera impossible de retrouver les privilèges." "Vous rétrograder ?" + "%1$s (En attente)" + "(En attente)" "Modifier les modérateurs" + "Administrateurs" + "Modérateurs" + "Membres" "Vous avez des modifications non-enregistrées." "Enregistrer les modifications?" "Ajouter un sujet" diff --git a/features/roomdetails/impl/src/main/res/values-hu/translations.xml b/features/roomdetails/impl/src/main/res/values-hu/translations.xml index 3640f1e853..66c05c38a7 100644 --- a/features/roomdetails/impl/src/main/res/values-hu/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-hu/translations.xml @@ -29,7 +29,11 @@ "Lefokozás" "Ezt a változtatást nem fogja tudni visszavonni, mivel lefokozza magát, ha Ön az utolsó jogosultságokkal rendelkező felhasználó a szobában, akkor lehetetlen lesz visszaszerezni a jogosultságokat." "Lefokozza magát?" + "%1$s (függőben)" "Moderátorok szerkesztése" + "Rendszergazdák" + "Moderátorok" + "Tagok" "Mentetlen módosításai vannak." "Menti a változtatásokat?" "Téma hozzáadása" diff --git a/features/roomdetails/impl/src/main/res/values-in/translations.xml b/features/roomdetails/impl/src/main/res/values-in/translations.xml index ba3e609d44..ddfd4859ea 100644 --- a/features/roomdetails/impl/src/main/res/values-in/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml @@ -29,7 +29,11 @@ "Turunkan" "Anda tidak akan dapat mengurungkan perubahan ini karena Anda sedang menurunkan Anda sendiri, jika Anda merupakan pengguna dengan hak khusus dalam ruangan maka tidak akan memungkinkan untuk mendapatkan hak tersebut lagi." "Turunkan Anda sendiri?" + "%1$s (Tertunda)" "Sunting Moderator" + "Admin" + "Moderator" + "Anggota" "Anda memiliki perubahan yang belum disimpan." "Simpan perubahan?" "Tambahkan topik" diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml index 02e7f03cba..77b46cdeda 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -30,6 +30,9 @@ "Non potrai annullare questa modifica perché ti stai declassando, se sei l\'ultimo utente privilegiato nella stanza, sarà impossibile riottenere i privilegi." "Declassare te stesso?" "Modifica moderatori" + "Amministratori" + "Moderatori" + "Membri" "Hai delle modifiche non salvate." "Salvare le modifiche?" "Aggiungi argomento" diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml index 787622103b..0f4c19b4d8 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -10,6 +10,7 @@ "Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere." "Sondaje" "Toți" + "Membri" "Adăugare subiect" "Deja membru" "Deja invitat" diff --git a/features/roomdetails/impl/src/main/res/values-ru/translations.xml b/features/roomdetails/impl/src/main/res/values-ru/translations.xml index 401b4a7c0c..200b2ef3c7 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -29,7 +29,11 @@ "Понизить уровень" "Вы не сможете отменить это изменение, так как понижаете себя статус. Если вы являетесь последним привилегированным пользователем в комнате, восстановить привилегии будет невозможно." "Понизить свой уровень?" + "%1$s (Ожидание)" "Редактировать роль модераторов" + "Администраторы" + "Модераторы" + "Участники" "У вас есть несохраненные изменения." "Сохранить изменения?" "Добавить тему" diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml index 78d4d842d9..c85b04017c 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -29,7 +29,11 @@ "Znížiť" "Túto zmenu nebudete môcť vrátiť späť, pretože znižujete svoju úroveň. Ak ste posledným privilegovaným používateľom v miestnosti, nebude možné získať znova oprávnenia." "Znížiť svoju úroveň?" + "%1$s (Čaká sa)" "Upraviť moderátorov" + "Správcovia" + "Moderátori" + "Členovia" "Máte neuložené zmeny." "Uložiť zmeny?" "Pridať tému" diff --git a/features/roomdetails/impl/src/main/res/values-uk/translations.xml b/features/roomdetails/impl/src/main/res/values-uk/translations.xml index b42e1a5e63..415334df5a 100644 --- a/features/roomdetails/impl/src/main/res/values-uk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-uk/translations.xml @@ -11,18 +11,18 @@ "Опитування" "Тільки для адміністраторів" "Заблоковувати людей" - "Видалити повідомлення" + "Вилучати повідомлення" "Усі" - "Запросити людей" + "Запрошувати людей" "Модерація учасників" "Повідомлення та зміст" "Адміністратори та модератори" - "Видалити людей" + "Вилучати людей" "Змінити аватар кімнати" "Деталі кімнати" "Змінити назву кімнати" "Змінити тему кімнати" - "Надіслати повідомлення" + "Надсилати повідомлення" "Керувати адмінами" "Ви не зможете скасувати цю дію. Ви просуваєте користувача, щоб він мав такий же рівень прав, як і ви." "Додати адміністратора?" @@ -30,6 +30,9 @@ "Ви не зможете скасувати цю зміну, оскільки ви знижуєте себе, якщо ви останній привілейований користувач у кімнаті, відновити привілеї буде неможливо." "Понизити себе?" "Керувати модераторами" + "Адміністратори" + "Модератори" + "Учасники" "У вас є не збережені зміни." "Зберегти зміни?" "Додати тему" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index cde723257f..8067676f93 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -29,7 +29,13 @@ "Demote" "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges." "Demote yourself?" + "%1$s (Pending)" + "(Pending)" + "Admins automatically have moderator privileges" "Edit Moderators" + "Admins" + "Moderators" + "Members" "You have unsaved changes." "Save changes?" "Add topic" @@ -53,6 +59,7 @@ "Room name" "Security" "Share room" + "Room info" "Topic" "Updating room…" "Ban" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 33f0b3fec0..243b6c0c4d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -32,9 +32,6 @@ import io.element.android.features.roomdetails.impl.members.moderation.aRoomMemb import io.element.android.features.roomdetails.members.moderation.FakeRoomMembersModerationPresenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -241,14 +238,12 @@ private fun TestScope.createPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), matrixRoom: MatrixRoom = FakeMatrixRoom(), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), - featureFlagService: FeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to true)), moderationPresenter: FakeRoomMembersModerationPresenter = FakeRoomMembersModerationPresenter(), navigator: RoomMemberListNavigator = object : RoomMemberListNavigator { } ) = RoomMemberListPresenter( room = matrixRoom, roomMemberListDataSource = roomMemberListDataSource, coroutineDispatchers = coroutineDispatchers, - featureFlagService = featureFlagService, roomMembersModerationPresenter = moderationPresenter, navigator = navigator ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTests.kt index 8d49eed353..e972c37ae4 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/moderation/DefaultRoomMembersModerationPresenterTests.kt @@ -28,8 +28,6 @@ import io.element.android.features.roomdetails.impl.members.moderation.Moderatio import io.element.android.features.roomdetails.impl.members.moderation.RoomMembersModerationEvents import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState @@ -45,13 +43,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Test class DefaultRoomMembersModerationPresenterTests { - @Test - fun `canDisplayModerationActions - when feature flag is disabled returns false`() = runTest { - val featureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to false)) - val presenter = createDefaultRoomMembersModerationPresenter(featureFlagService = featureFlagService) - assertThat(presenter.canDisplayModerationActions()).isFalse() - } - @Test fun `canDisplayModerationActions - when room is DM is false`() = runTest { val room = FakeMatrixRoom(isDirect = true, isPublic = true, isOneToOne = true).apply { @@ -309,13 +300,11 @@ class DefaultRoomMembersModerationPresenterTests { private fun TestScope.createDefaultRoomMembersModerationPresenter( matrixRoom: FakeMatrixRoom = FakeMatrixRoom(), - featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(initialState = mapOf(FeatureFlags.RoomModeration.key to true)), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), ): DefaultRoomMembersModerationPresenter { return DefaultRoomMembersModerationPresenter( room = matrixRoom, - featureFlagService = featureFlagService, dispatchers = dispatchers, analyticsService = analyticsService, ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTests.kt index ff5e185bbd..e8ff232222 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesPresenterTests.kt @@ -21,7 +21,6 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.RoomModeration -import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.aRoomMemberList import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesPresenter @@ -30,6 +29,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.user.MatrixUser 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.room.FakeMatrixRoom @@ -106,15 +106,19 @@ class ChangeRolesPresenterTests { presenter.present() }.test { val initialState = awaitItem() - val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty() - assertThat(initialResults).hasSize(10) + val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results + assertThat(initialResults?.members).hasSize(8) + assertThat(initialResults?.moderators).hasSize(1) + assertThat(initialResults?.admins).hasSize(1) initialState.eventSink(ChangeRolesEvent.QueryChanged("Alice")) skipItems(1) - val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty() - assertThat(searchResults).hasSize(1) - assertThat(searchResults.firstOrNull()?.userId).isEqualTo(A_USER_ID) + val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results + assertThat(searchResults?.admins).hasSize(1) + assertThat(searchResults?.moderators).isEmpty() + assertThat(searchResults?.members).isEmpty() + assertThat(searchResults?.admins?.firstOrNull()?.userId).isEqualTo(A_USER_ID) } } @@ -128,15 +132,19 @@ class ChangeRolesPresenterTests { presenter.present() }.test { skipItems(1) - val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty() - assertThat(initialResults).hasSize(10) + val initialResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results + assertThat(initialResults?.members).hasSize(8) + assertThat(initialResults?.moderators).hasSize(1) + assertThat(initialResults?.admins).hasSize(1) room.givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList().take(1).toPersistentList())) skipItems(1) - val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results.orEmpty() - assertThat(searchResults).hasSize(1) - assertThat(searchResults.firstOrNull()?.userId).isEqualTo(A_USER_ID) + val searchResults = (awaitItem().searchResults as? SearchBarResultState.Results)?.results + assertThat(searchResults?.admins).hasSize(1) + assertThat(searchResults?.moderators).isEmpty() + assertThat(searchResults?.members).isEmpty() + assertThat(searchResults?.admins?.firstOrNull()?.userId).isEqualTo(A_USER_ID) } } @@ -154,10 +162,10 @@ class ChangeRolesPresenterTests { val initialState = awaitItem() assertThat(initialState.selectedUsers).hasSize(1) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) assertThat(awaitItem().selectedUsers).hasSize(2) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) assertThat(awaitItem().selectedUsers).hasSize(1) } } @@ -177,13 +185,13 @@ class ChangeRolesPresenterTests { assertThat(initialState.hasPendingChanges).isFalse() assertThat(initialState.selectedUsers).hasSize(1) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) with(awaitItem()) { assertThat(selectedUsers).hasSize(2) assertThat(hasPendingChanges).isTrue() } - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) with(awaitItem()) { assertThat(selectedUsers).hasSize(1) assertThat(hasPendingChanges).isFalse() @@ -226,7 +234,7 @@ class ChangeRolesPresenterTests { assertThat(initialState.hasPendingChanges).isFalse() assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) awaitItem().eventSink(ChangeRolesEvent.Exit) val confirmingState = awaitItem() @@ -252,7 +260,7 @@ class ChangeRolesPresenterTests { assertThat(initialState.hasPendingChanges).isFalse() assertThat(initialState.exitState).isEqualTo(AsyncAction.Uninitialized) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) val updatedState = awaitItem() assertThat(updatedState.hasPendingChanges).isTrue() skipItems(1) @@ -279,8 +287,7 @@ class ChangeRolesPresenterTests { val initialState = awaitItem() assertThat(initialState.selectedUsers).hasSize(1) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) - + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) awaitItem().eventSink(ChangeRolesEvent.Save) val confirmingState = awaitItem() assertThat(confirmingState.savingState).isEqualTo(AsyncAction.Confirming) @@ -304,7 +311,7 @@ class ChangeRolesPresenterTests { val initialState = awaitItem() assertThat(initialState.selectedUsers).hasSize(1) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) awaitItem().eventSink(ChangeRolesEvent.Save) val confirmingState = awaitItem() @@ -334,7 +341,7 @@ class ChangeRolesPresenterTests { val initialState = awaitItem() assertThat(initialState.selectedUsers).hasSize(1) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) awaitItem().eventSink(ChangeRolesEvent.Save) assertThat(awaitItem().savingState).isEqualTo(AsyncAction.Success(Unit)) @@ -357,7 +364,7 @@ class ChangeRolesPresenterTests { val initialState = awaitItem() assertThat(initialState.selectedUsers).hasSize(1) - initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(aRoomMember(A_USER_ID_2))) + initialState.eventSink(ChangeRolesEvent.UserSelectionToggled(MatrixUser(A_USER_ID_2))) awaitItem().eventSink(ChangeRolesEvent.Save) val failedState = awaitItem() diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesViewTest.kt new file mode 100644 index 0000000000..93cbad4d58 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/ChangeRolesViewTest.kt @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.rolesandpermissions.changeroles + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesEvent +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesState +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.ChangeRolesView +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.aChangeRolesState +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.aChangeRolesStateWithSelectedUsers +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.toMatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.pressBack +import io.element.android.tests.testutils.pressBackKey +import kotlinx.collections.immutable.toImmutableList +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.lang.IllegalStateException + +@RunWith(AndroidJUnit4::class) +class ChangeRolesViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `passing a 'USER' role throws an exception`() { + val exception = runCatching { + rule.setChangeRolesContent( + state = aChangeRolesState( + role = RoomMember.Role.USER, + eventSink = EnsureNeverCalledWithParam(), + ), + ) + }.exceptionOrNull() + + assertThat(exception).isNotNull() + } + + @Test + fun `back key - with search active toggles the search`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + isSearchActive = true, + eventSink = eventsRecorder, + ), + ) + + rule.pressBackKey() + + eventsRecorder.assertSingle(ChangeRolesEvent.ToggleSearchActive) + } + + @Test + fun `back key - with search inactive exits the screen`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + isSearchActive = false, + eventSink = eventsRecorder, + ), + ) + + rule.pressBackKey() + + eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Exit)) + } + + @Test + fun `back button - exits the screen`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + isSearchActive = false, + eventSink = eventsRecorder, + ), + ) + + rule.pressBack() + + eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Exit)) + } + + @Test + fun `save button - with changes, it saves them`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + hasPendingChanges = true, + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(CommonStrings.action_save) + + eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""), ChangeRolesEvent.Save)) + } + + @Test + fun `save button - with no changes, does nothing`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + hasPendingChanges = false, + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(CommonStrings.action_save) + + eventsRecorder.assertList(listOf(ChangeRolesEvent.QueryChanged(""))) + } + + @Test + fun `exit confirmation dialog - submit exits the screen`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + isSearchActive = true, + exitState = AsyncAction.Confirming, + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(CommonStrings.action_ok) + + eventsRecorder.assertSingle(ChangeRolesEvent.Exit) + } + + @Test + fun `exit confirmation dialog - cancel removes the dialog`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + isSearchActive = true, + exitState = AsyncAction.Confirming, + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(CommonStrings.action_cancel) + + eventsRecorder.assertSingle(ChangeRolesEvent.CancelExit) + } + + @Test + fun `save confirmation dialog - submit saves the changes`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + role = RoomMember.Role.ADMIN, + isSearchActive = true, + savingState = AsyncAction.Confirming, + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(CommonStrings.action_ok) + + eventsRecorder.assertSingle(ChangeRolesEvent.Save) + } + + @Test + fun `save confirmation dialog - cancel removes the dialog`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + role = RoomMember.Role.ADMIN, + isSearchActive = true, + savingState = AsyncAction.Confirming, + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(CommonStrings.action_cancel) + + eventsRecorder.assertSingle(ChangeRolesEvent.ClearError) + } + + @Test + fun `error dialog - dismissing removes the dialog`() { + val eventsRecorder = EventsRecorder() + rule.setChangeRolesContent( + state = aChangeRolesState( + isSearchActive = true, + savingState = AsyncAction.Failure(IllegalStateException("boom")), + eventSink = eventsRecorder, + ), + ) + + rule.clickOn(CommonStrings.action_ok) + + eventsRecorder.assertSingle(ChangeRolesEvent.ClearError) + } + + @Test + fun `testing removing user from selected list emits the expected event`() { + val eventsRecorder = EventsRecorder() + val selectedUsers = aMatrixUserList().take(2) + val userToDeselect = selectedUsers[1] + assertThat(userToDeselect.displayName).isEqualTo("Bob") + rule.setChangeRolesContent( + state = aChangeRolesStateWithSelectedUsers().copy( + selectedUsers = selectedUsers.toImmutableList(), + eventSink = eventsRecorder, + ), + ) + // Unselect the user from the row list + val contentDescription = rule.activity.getString(CommonStrings.action_remove) + rule.onNodeWithContentDescription(contentDescription).performClick() + eventsRecorder.assertList( + listOf( + ChangeRolesEvent.QueryChanged(""), + ChangeRolesEvent.UserSelectionToggled(userToDeselect), + ) + ) + } + + @Test + @Config(qualifiers = "h1000dp") + fun `testing adding user to the selected list emits the expected event`() { + val eventsRecorder = EventsRecorder() + val selectedUsers = aMatrixUserList().take(2) + val state = aChangeRolesStateWithSelectedUsers().copy( + selectedUsers = selectedUsers.toImmutableList(), + eventSink = eventsRecorder, + ) + val userToSelect = (state.searchResults as SearchBarResultState.Results).results.members.first().toMatrixUser() + assertThat(userToSelect.displayName).isEqualTo("Carol") + rule.setChangeRolesContent( + state = state, + ) + // Select the user from the row list + rule.onNodeWithText("Carol").performClick() + eventsRecorder.assertList( + listOf( + ChangeRolesEvent.QueryChanged(""), + ChangeRolesEvent.UserSelectionToggled(userToSelect), + ) + ) + } + + @Test + fun `testing removing user to the selected list emits the expected event`() { + val eventsRecorder = EventsRecorder() + val selectedUsers = aMatrixUserList().take(2) + val state = aChangeRolesStateWithSelectedUsers().copy( + selectedUsers = selectedUsers.toImmutableList(), + eventSink = eventsRecorder, + ) + val userToSelect = (state.searchResults as SearchBarResultState.Results).results.moderators.first().toMatrixUser() + assertThat(userToSelect.displayName).isEqualTo("Bob") + rule.setChangeRolesContent( + state = state, + ) + // Select the user from the rom list + rule.onAllNodesWithText("Bob")[1].performClick() + eventsRecorder.assertList( + listOf( + ChangeRolesEvent.QueryChanged(""), + ChangeRolesEvent.UserSelectionToggled(userToSelect), + ) + ) + } + + private fun AndroidComposeTestRule.setChangeRolesContent( + state: ChangeRolesState, + onBackPressed: () -> Unit = EnsureNeverCalled(), + ) { + setContent { + ChangeRolesView( + state = state, + navigateUp = onBackPressed, + ) + } + } +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/MembersByRoleTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/MembersByRoleTest.kt new file mode 100644 index 0000000000..7819745c52 --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/rolesandpermissions/changeroles/MembersByRoleTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.rolesandpermissions.changeroles + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdetails.impl.rolesandpermissions.changeroles.MembersByRole +import io.element.android.libraries.matrix.api.room.RoomMember +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.A_USER_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_4 +import io.element.android.libraries.matrix.test.A_USER_ID_5 +import io.element.android.libraries.matrix.test.room.aRoomMember +import kotlinx.collections.immutable.persistentListOf +import org.junit.Test + +class MembersByRoleTest { + @Test + fun `constructor - with single member list categorizes and sorts members`() { + val members = listOf( + aRoomMember(A_USER_ID_2, displayName = "Bob", role = RoomMember.Role.ADMIN), + aRoomMember(A_USER_ID, displayName = "Alice", role = RoomMember.Role.ADMIN), + aRoomMember(A_USER_ID_3, displayName = "Carol", role = RoomMember.Role.USER), + aRoomMember(A_USER_ID_5, displayName = "Eve", role = RoomMember.Role.USER), + aRoomMember(A_USER_ID_4, displayName = "David", role = RoomMember.Role.USER), + ) + val membersByRole = MembersByRole(members = members) + assertThat(membersByRole.admins).containsExactly( + aRoomMember(A_USER_ID, displayName = "Alice", role = RoomMember.Role.ADMIN), + aRoomMember(A_USER_ID_2, displayName = "Bob", role = RoomMember.Role.ADMIN), + ) + assertThat(membersByRole.moderators).isEmpty() + assertThat(membersByRole.members).containsExactly( + aRoomMember(A_USER_ID_3, displayName = "Carol", role = RoomMember.Role.USER), + aRoomMember(A_USER_ID_4, displayName = "David", role = RoomMember.Role.USER), + aRoomMember(A_USER_ID_5, displayName = "Eve", role = RoomMember.Role.USER), + ) + } + + @Test + fun `isEmpty - only returns true with no members of any role`() { + val emptyMembersByRole = MembersByRole(emptyList()) + assertThat(emptyMembersByRole.isEmpty()).isTrue() + + val membersByRoleWithAdmins = MembersByRole( + admins = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.ADMIN)), + moderators = persistentListOf(), + members = persistentListOf(), + ) + assertThat(membersByRoleWithAdmins.isEmpty()).isFalse() + + val membersByRoleWithModerators = MembersByRole( + admins = persistentListOf(), + moderators = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.MODERATOR)), + members = persistentListOf(), + ) + assertThat(membersByRoleWithModerators.isEmpty()).isFalse() + + val membersByRoleWithMembers = MembersByRole( + admins = persistentListOf(), + moderators = persistentListOf(), + members = persistentListOf(aRoomMember(A_USER_ID, role = RoomMember.Role.USER)), + ) + assertThat(membersByRoleWithMembers.isEmpty()).isFalse() + } +} diff --git a/features/roomdirectory/api/build.gradle.kts b/features/roomdirectory/api/build.gradle.kts new file mode 100644 index 0000000000..04a813bd0f --- /dev/null +++ b/features/roomdirectory/api/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.roomdirectory.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.designsystem) +} diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt new file mode 100644 index 0000000000..5b945b2a7d --- /dev/null +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDescription.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.api + +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.api.core.RoomId + +data class RoomDescription( + val roomId: RoomId, + val name: String, + val description: String, + val avatarData: AvatarData, + val canBeJoined: Boolean, +) diff --git a/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt new file mode 100644 index 0000000000..5a693a4a83 --- /dev/null +++ b/features/roomdirectory/api/src/main/kotlin/io/element/android/features/roomdirectory/api/RoomDirectoryEntryPoint.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId + +interface RoomDirectoryEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onOpenRoom(roomId: RoomId) + } +} diff --git a/features/roomdirectory/impl/build.gradle.kts b/features/roomdirectory/impl/build.gradle.kts new file mode 100644 index 0000000000..85bb195da3 --- /dev/null +++ b/features/roomdirectory/impl/build.gradle.kts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.roomdirectory.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.roomdirectory.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.testtags) + + testImplementation(libs.test.junit) + testImplementation(libs.androidx.compose.ui.test.junit) + testImplementation(libs.test.robolectric) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) + + ksp(libs.showkase.processor) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt new file mode 100644 index 0000000000..c15a748a9e --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/DefaultRoomDirectoryEntryPoint.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint +import io.element.android.features.roomdirectory.impl.root.RoomDirectoryNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultRoomDirectoryEntryPoint @Inject constructor() : RoomDirectoryEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDirectoryEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : RoomDirectoryEntryPoint.NodeBuilder { + override fun callback(callback: RoomDirectoryEntryPoint.Callback): RoomDirectoryEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt new file mode 100644 index 0000000000..37e0ffb3c6 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface RoomDirectoryEvents { + data class JoinRoom(val roomId: RoomId) : RoomDirectoryEvents + data class Search(val query: String) : RoomDirectoryEvents + data object LoadMore : RoomDirectoryEvents + data object JoinRoomDismissError : RoomDirectoryEvents +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt new file mode 100644 index 0000000000..dc3581589e --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryNode.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +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.roomdirectory.api.RoomDirectoryEntryPoint +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId + +@ContributesNode(SessionScope::class) +class RoomDirectoryNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: RoomDirectoryPresenter, +) : Node(buildContext, plugins = plugins) { + private fun onRoomJoined(roomId: RoomId) { + plugins().forEach { + it.onOpenRoom(roomId) + } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + RoomDirectoryView( + state = state, + onRoomJoined = ::onRoomJoined, + onBackPressed = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt new file mode 100644 index 0000000000..5d4cef55cb --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.roomdirectory.impl.root.di.JoinRoom +import io.element.android.features.roomdirectory.impl.root.model.RoomDirectoryListState +import io.element.android.features.roomdirectory.impl.root.model.toFeatureModel +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +class RoomDirectoryPresenter @Inject constructor( + private val dispatchers: CoroutineDispatchers, + private val joinRoom: JoinRoom, + private val roomDirectoryService: RoomDirectoryService, +) : Presenter { + @Composable + override fun present(): RoomDirectoryState { + var loadingMore by remember { + mutableStateOf(false) + } + var searchQuery by rememberSaveable { + mutableStateOf(null) + } + val coroutineScope = rememberCoroutineScope() + val roomDirectoryList = remember { + roomDirectoryService.createRoomDirectoryList(coroutineScope) + } + val listState by roomDirectoryList.collectState() + val joinRoomAction: MutableState> = remember { + mutableStateOf(AsyncAction.Uninitialized) + } + LaunchedEffect(searchQuery) { + if (searchQuery == null) return@LaunchedEffect + // cancel load more right away + loadingMore = false + // debounce search query + delay(300) + roomDirectoryList.filter(searchQuery, 20) + } + LaunchedEffect(loadingMore) { + if (loadingMore) { + roomDirectoryList.loadMore() + loadingMore = false + } + } + fun handleEvents(event: RoomDirectoryEvents) { + when (event) { + RoomDirectoryEvents.LoadMore -> { + loadingMore = true + } + is RoomDirectoryEvents.Search -> { + searchQuery = event.query + } + is RoomDirectoryEvents.JoinRoom -> { + coroutineScope.joinRoom(joinRoomAction, event.roomId) + } + RoomDirectoryEvents.JoinRoomDismissError -> { + joinRoomAction.value = AsyncAction.Uninitialized + } + } + } + + return RoomDirectoryState( + query = searchQuery.orEmpty(), + roomDescriptions = listState.items, + displayLoadMoreIndicator = listState.hasMoreToLoad, + joinRoomAction = joinRoomAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.joinRoom(state: MutableState>, roomId: RoomId) = launch { + state.runUpdatingState { + joinRoom(roomId) + } + } + + @Composable + private fun RoomDirectoryList.collectState() = remember { + state.map { + val items = it.items + .map { roomDescription -> roomDescription.toFeatureModel() } + .toImmutableList() + RoomDirectoryListState(items = items, hasMoreToLoad = it.hasMoreToLoad) + }.flowOn(dispatchers.computation) + }.collectAsState(RoomDirectoryListState.Default) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt new file mode 100644 index 0000000000..526139338d --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +import io.element.android.features.roomdirectory.api.RoomDescription +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.collections.immutable.ImmutableList + +data class RoomDirectoryState( + val query: String, + val roomDescriptions: ImmutableList, + val displayLoadMoreIndicator: Boolean, + val joinRoomAction: AsyncAction, + val eventSink: (RoomDirectoryEvents) -> Unit +) { + val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt new file mode 100644 index 0000000000..efb6624260 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.roomdirectory.api.RoomDescription +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +open class RoomDirectoryStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomDirectoryState(), + aRoomDirectoryState( + query = "Element", + roomDescriptions = aRoomDescriptionList(), + ), + aRoomDirectoryState( + query = "Element", + roomDescriptions = aRoomDescriptionList(), + displayLoadMoreIndicator = true, + ), + aRoomDirectoryState( + query = "Element", + roomDescriptions = aRoomDescriptionList(), + joinRoomAction = AsyncAction.Loading, + ), + aRoomDirectoryState( + query = "Element", + roomDescriptions = aRoomDescriptionList(), + joinRoomAction = AsyncAction.Failure(Exception("Failed to join room")), + ), + ) +} + +fun aRoomDirectoryState( + query: String = "", + displayLoadMoreIndicator: Boolean = false, + roomDescriptions: ImmutableList = persistentListOf(), + joinRoomAction: AsyncAction = AsyncAction.Uninitialized, + eventSink: (RoomDirectoryEvents) -> Unit = {}, +) = RoomDirectoryState( + query = query, + roomDescriptions = roomDescriptions, + displayLoadMoreIndicator = displayLoadMoreIndicator, + joinRoomAction = joinRoomAction, + eventSink = eventSink, +) + +fun aRoomDescriptionList(): ImmutableList { + return persistentListOf( + RoomDescription( + roomId = RoomId("!exa:matrix.org"), + name = "Element X Android", + description = "Element X is a secure, private and decentralized messenger.", + avatarData = AvatarData( + id = "!exa:matrix.org", + name = "Element X Android", + url = null, + size = AvatarSize.RoomDirectoryItem + ), + canBeJoined = true, + ), + RoomDescription( + roomId = RoomId("!exi:matrix.org"), + name = "Element X iOS", + description = "Element X is a secure, private and decentralized messenger.", + avatarData = AvatarData( + id = "!exi:matrix.org", + name = "Element X iOS", + url = null, + size = AvatarSize.RoomDirectoryItem + ), + canBeJoined = false, + ) + ) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt new file mode 100644 index 0000000000..f6188c3269 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.roomdirectory.api.RoomDescription +import io.element.android.features.roomdirectory.impl.R +import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun RoomDirectoryView( + state: RoomDirectoryState, + onRoomJoined: (RoomId) -> Unit, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + fun joinRoom(roomId: RoomId) { + state.eventSink(RoomDirectoryEvents.JoinRoom(roomId)) + } + + Scaffold( + modifier = modifier, + topBar = { + RoomDirectoryTopBar(onBackPressed = onBackPressed) + }, + content = { padding -> + RoomDirectoryContent( + state = state, + onResultClicked = ::joinRoom, + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + ) + } + ) + AsyncActionView( + async = state.joinRoomAction, + onSuccess = onRoomJoined, + onErrorDismiss = { + state.eventSink(RoomDirectoryEvents.JoinRoomDismissError) + }, + errorMessage = { + stringResource(id = CommonStrings.error_unknown) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RoomDirectoryTopBar( + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + navigationIcon = { + BackButton(onClick = onBackPressed) + }, + title = { + Text( + text = stringResource(id = R.string.screen_room_directory_search_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + } + ) +} + +@Composable +private fun RoomDirectoryContent( + state: RoomDirectoryState, + onResultClicked: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + SearchTextField( + query = state.query, + onQueryChange = { state.eventSink(RoomDirectoryEvents.Search(it)) }, + placeholder = stringResource(id = CommonStrings.action_search), + modifier = Modifier.fillMaxWidth(), + ) + RoomDirectoryRoomList( + roomDescriptions = state.roomDescriptions, + displayLoadMoreIndicator = state.displayLoadMoreIndicator, + displayEmptyState = state.displayEmptyState, + onResultClicked = onResultClicked, + onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) }, + ) + } +} + +@Composable +private fun RoomDirectoryRoomList( + roomDescriptions: ImmutableList, + displayLoadMoreIndicator: Boolean, + displayEmptyState: Boolean, + onResultClicked: (RoomId) -> Unit, + onReachedLoadMore: () -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + items(roomDescriptions) { roomDescription -> + RoomDirectoryRoomRow( + roomDescription = roomDescription, + onClick = onResultClicked, + ) + } + if (displayEmptyState) { + item { + Text( + text = stringResource(id = CommonStrings.common_no_results), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPlaceholder, + modifier = Modifier.padding(16.dp) + ) + } + } + if (displayLoadMoreIndicator) { + item { + LoadMoreIndicator(modifier = Modifier.fillMaxWidth()) + LaunchedEffect(onReachedLoadMore) { + onReachedLoadMore() + } + } + } + } +} + +@Composable +private fun LoadMoreIndicator(modifier: Modifier = Modifier) { + Box( + modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(24.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + strokeWidth = 2.dp, + ) + } +} + +@Composable +private fun SearchTextField( + query: String, + onQueryChange: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + colors: TextFieldColors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + unfocusedPlaceholderColor = ElementTheme.colors.textPlaceholder, + focusedPlaceholderColor = ElementTheme.colors.textPlaceholder, + focusedTextColor = ElementTheme.colors.textPrimary, + unfocusedTextColor = ElementTheme.colors.textPrimary, + focusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary, + unfocusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary, + ), +) { + val focusManager = LocalFocusManager.current + TextField( + modifier = modifier.testTag(TestTags.searchTextField.value), + textStyle = ElementTheme.typography.fontBodyLgRegular, + singleLine = true, + value = query, + onValueChange = onQueryChange, + keyboardActions = KeyboardActions( + onSearch = { + focusManager.clearFocus() + } + ), + colors = colors, + placeholder = { Text(placeholder) }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton( + onClick = { + onQueryChange("") + } + ) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_clear), + ) + } + } else { + Icon( + imageVector = CompoundIcons.Search(), + contentDescription = stringResource(CommonStrings.action_search), + ) + } + }, + ) +} + +@Composable +private fun RoomDirectoryRoomRow( + roomDescription: RoomDescription, + onClick: (RoomId) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(enabled = roomDescription.canBeJoined) { + onClick(roomDescription.roomId) + } + .padding( + top = 12.dp, + bottom = 12.dp, + start = 16.dp, + ) + .height(IntrinsicSize.Min), + ) { + Avatar( + avatarData = roomDescription.avatarData, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) { + Text( + text = roomDescription.name, + maxLines = 1, + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = roomDescription.description, + maxLines = 1, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textSecondary, + overflow = TextOverflow.Ellipsis, + ) + } + if (roomDescription.canBeJoined) { + Text( + text = stringResource(id = CommonStrings.action_join), + color = ElementTheme.colors.textSuccessPrimary, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 4.dp, end = 12.dp) + ) + } else { + Spacer(modifier = Modifier.width(24.dp)) + } + } +} + +@PreviewsDayNight +@Composable +internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview { + RoomDirectoryView( + state = state, + onRoomJoined = {}, + onBackPressed = {}, + ) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt new file mode 100644 index 0000000000..983d2a1dd2 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root.di + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import javax.inject.Inject + +interface JoinRoom { + suspend operator fun invoke(roomId: RoomId): Result +} + +@ContributesBinding(SessionScope::class) +class DefaultJoinRoom @Inject constructor(private val client: MatrixClient) : JoinRoom { + override suspend fun invoke(roomId: RoomId) = client.joinRoom(roomId) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt new file mode 100644 index 0000000000..be36eb5053 --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDescription.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root.model + +import io.element.android.features.roomdirectory.api.RoomDescription +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription + +fun MatrixRoomDescription.toFeatureModel(): RoomDescription { + fun name(): String { + return name ?: alias ?: roomId.value + } + + fun description(): String { + val topic = topic + val alias = alias + val name = name + return when { + topic != null -> topic + name != null && alias != null -> alias + name == null && alias == null -> "" + else -> roomId.value + } + } + + return RoomDescription( + roomId = roomId, + name = name(), + description = description(), + avatarData = AvatarData( + id = roomId.value, + name = name, + url = avatarUrl, + size = AvatarSize.RoomDirectoryItem, + ), + canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC, + ) +} diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt new file mode 100644 index 0000000000..60f344f67b --- /dev/null +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/model/RoomDirectoryListState.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root.model + +import io.element.android.features.roomdirectory.api.RoomDescription +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal data class RoomDirectoryListState( + val hasMoreToLoad: Boolean, + val items: ImmutableList, +) { + companion object { + val Default = RoomDirectoryListState( + hasMoreToLoad = true, + items = persistentListOf() + ) + } +} diff --git a/features/roomdirectory/impl/src/main/res/values-be/translations.xml b/features/roomdirectory/impl/src/main/res/values-be/translations.xml new file mode 100644 index 0000000000..16b52ab04d --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,5 @@ + + + "Памылка загрузкі" + "Каталог пакояў" + diff --git a/features/roomdirectory/impl/src/main/res/values-cs/translations.xml b/features/roomdirectory/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..49ae689e43 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,5 @@ + + + "Načítání se nezdařilo" + "Adresář místností" + diff --git a/features/roomdirectory/impl/src/main/res/values-de/translations.xml b/features/roomdirectory/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..8c60d845b2 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,5 @@ + + + "Fehler beim Laden" + "Raumverzeichnis" + diff --git a/features/roomdirectory/impl/src/main/res/values-fr/translations.xml b/features/roomdirectory/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..25c8064237 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,5 @@ + + + "Échec du chargement" + "Annuaire des salons" + diff --git a/features/roomdirectory/impl/src/main/res/values-hu/translations.xml b/features/roomdirectory/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..fcaa3eb1bf --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,5 @@ + + + "Sikertelen betöltés" + "Szobakatalógus" + diff --git a/features/roomdirectory/impl/src/main/res/values-ru/translations.xml b/features/roomdirectory/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..92e16fadaa --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,5 @@ + + + "Сбой загрузки" + "Каталог комнат" + diff --git a/features/roomdirectory/impl/src/main/res/values-sk/translations.xml b/features/roomdirectory/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..9d4e0bfd61 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,5 @@ + + + "Načítanie zlyhalo" + "Adresár miestností" + diff --git a/features/roomdirectory/impl/src/main/res/values-uk/translations.xml b/features/roomdirectory/impl/src/main/res/values-uk/translations.xml new file mode 100644 index 0000000000..4201f040e9 --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values-uk/translations.xml @@ -0,0 +1,5 @@ + + + "Не вдалося завантажити" + "Каталог кімнат" + diff --git a/features/roomdirectory/impl/src/main/res/values/localazy.xml b/features/roomdirectory/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..d3fb9d15ae --- /dev/null +++ b/features/roomdirectory/impl/src/main/res/values/localazy.xml @@ -0,0 +1,5 @@ + + + "Failed loading" + "Room directory" + diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt new file mode 100644 index 0000000000..3f4d17aefd --- /dev/null +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +import io.element.android.features.roomdirectory.impl.root.di.JoinRoom +import io.element.android.libraries.matrix.api.core.RoomId + +class FakeJoinRoom( + var lambda: (RoomId) -> Result = { Result.success(it) } +) : JoinRoom { + override suspend fun invoke(roomId: RoomId) = lambda(roomId) +} diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt new file mode 100644 index 0000000000..eefafc86e1 --- /dev/null +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.roomdirectory.impl.root.di.JoinRoom +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryList +import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService +import io.element.android.libraries.matrix.test.roomdirectory.aRoomDescription +import io.element.android.tests.testutils.lambda.any +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 io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) class RoomDirectoryPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createRoomDirectoryPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.query).isEmpty() + assertThat(initialState.displayEmptyState).isFalse() + assertThat(initialState.joinRoomAction).isEqualTo(AsyncAction.Uninitialized) + assertThat(initialState.roomDescriptions).isEmpty() + assertThat(initialState.displayLoadMoreIndicator).isTrue() + } + } + + @Test + fun `present - room directory list emits empty state`() = runTest { + val directoryListStateFlow = MutableSharedFlow(replay = 1) + val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow) + val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } + val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) + presenter.test { + skipItems(1) + directoryListStateFlow.emit( + RoomDirectoryList.State(false, emptyList()) + ) + awaitItem().also { state -> + assertThat(state.displayEmptyState).isTrue() + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - room directory list emits non-empty state`() = runTest { + val directoryListStateFlow = MutableSharedFlow(replay = 1) + val roomDirectoryList = FakeRoomDirectoryList(directoryListStateFlow) + val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } + val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) + presenter.test { + skipItems(1) + directoryListStateFlow.emit( + RoomDirectoryList.State( + hasMoreToLoad = true, + items = listOf(aRoomDescription()) + ) + ) + awaitItem().also { state -> + assertThat(state.displayEmptyState).isFalse() + assertThat(state.roomDescriptions).hasSize(1) + } + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - emit search event`() = runTest { + val filterLambda = lambdaRecorder { _: String?, _: Int -> + Result.success(Unit) + } + val roomDirectoryList = FakeRoomDirectoryList(filterLambda = filterLambda) + val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } + val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) + presenter.test { + awaitItem().also { state -> + state.eventSink(RoomDirectoryEvents.Search("test")) + } + awaitItem().also { state -> + assertThat(state.query).isEqualTo("test") + } + advanceUntilIdle() + cancelAndIgnoreRemainingEvents() + } + assert(filterLambda) + .isCalledOnce() + .with(value("test"), any()) + } + + @Test + fun `present - emit load more event`() = runTest { + val loadMoreLambda = lambdaRecorder { -> + Result.success(Unit) + } + val roomDirectoryList = FakeRoomDirectoryList(loadMoreLambda = loadMoreLambda) + val roomDirectoryService = FakeRoomDirectoryService { roomDirectoryList } + val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) + presenter.test { + awaitItem().also { state -> + state.eventSink(RoomDirectoryEvents.LoadMore) + } + advanceUntilIdle() + cancelAndIgnoreRemainingEvents() + } + assert(loadMoreLambda) + .isCalledOnce() + .withNoParameter() + } + + @Test + fun `present - emit join room event`() = runTest { + val joinRoomSuccess = lambdaRecorder { roomId: RoomId -> + Result.success(roomId) + } + val joinRoomFailure = lambdaRecorder { roomId: RoomId -> + Result.failure(RuntimeException("Failed to join room $roomId")) + } + val fakeJoinRoom = FakeJoinRoom(joinRoomSuccess) + val presenter = createRoomDirectoryPresenter(joinRoom = fakeJoinRoom) + presenter.test { + awaitItem().also { state -> + state.eventSink(RoomDirectoryEvents.JoinRoom(A_ROOM_ID)) + } + awaitItem().also { state -> + assertThat(state.joinRoomAction).isEqualTo(AsyncAction.Success(A_ROOM_ID)) + fakeJoinRoom.lambda = joinRoomFailure + state.eventSink(RoomDirectoryEvents.JoinRoom(A_ROOM_ID)) + } + awaitItem().also { state -> + assertThat(state.joinRoomAction).isInstanceOf(AsyncAction.Failure::class.java) + } + } + assert(joinRoomSuccess) + .isCalledOnce() + .with(value(A_ROOM_ID)) + assert(joinRoomFailure) + .isCalledOnce() + .with(value(A_ROOM_ID)) + } + + private fun TestScope.createRoomDirectoryPresenter( + roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService( + createRoomDirectoryListFactory = { FakeRoomDirectoryList() } + ), + joinRoom: JoinRoom = FakeJoinRoom { Result.success(it) }, + ): RoomDirectoryPresenter { + return RoomDirectoryPresenter( + dispatchers = testCoroutineDispatchers(), + joinRoom = joinRoom, + roomDirectoryService = roomDirectoryService, + ) + } +} diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt new file mode 100644 index 0000000000..bcac35fc3a --- /dev/null +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdirectory.impl.root + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.testtags.TestTags +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RoomDirectoryViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `typing text in search field emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomDirectoryView( + state = aRoomDirectoryState( + eventSink = eventsRecorder, + ) + ) + rule.onNodeWithTag(TestTags.searchTextField.value).performTextInput( + text = "Test" + ) + eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test")) + } + + @Test + fun `clicking on room item emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomDirectoryState( + roomDescriptions = aRoomDescriptionList(), + eventSink = eventsRecorder, + ) + rule.setRoomDirectoryView(state = state) + val clickedRoom = state.roomDescriptions.first() + rule.onNodeWithText(clickedRoom.name).performClick() + eventsRecorder.assertSingle(RoomDirectoryEvents.JoinRoom(clickedRoom.roomId)) + } + + @Test + fun `composing load more indicator emits expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomDirectoryState( + displayLoadMoreIndicator = true, + eventSink = eventsRecorder, + ) + rule.setRoomDirectoryView(state = state) + eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore) + } + + @Test + fun `when joining room with success then onRoomJoined lambda is called once`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val roomDescriptions = aRoomDescriptionList() + val joinedRoomId = roomDescriptions.first().roomId + val state = aRoomDirectoryState( + joinRoomAction = AsyncAction.Success(joinedRoomId), + roomDescriptions = roomDescriptions, + eventSink = eventsRecorder, + ) + ensureCalledOnceWithParam(joinedRoomId) { callback -> + rule.setRoomDirectoryView( + state = state, + onRoomJoined = callback, + ) + } + } +} + +private fun AndroidComposeTestRule.setRoomDirectoryView( + state: RoomDirectoryState, + onBackPressed: () -> Unit = EnsureNeverCalled(), + onRoomJoined: (RoomId) -> Unit = EnsureNeverCalledWithParam(), +) { + setContent { + RoomDirectoryView( + state = state, + onRoomJoined = onRoomJoined, + onBackPressed = onBackPressed, + ) + } +} 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 0404ce18fc..b5d1c1299f 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 @@ -33,10 +33,10 @@ interface RoomListEntryPoint : FeatureEntryPoint { fun onRoomClicked(roomId: RoomId) fun onCreateRoomClicked() fun onSettingsClicked() - fun onSessionVerificationClicked() fun onSessionConfirmRecoveryKeyClicked() fun onInvitesClicked() fun onRoomSettingsClicked(roomId: RoomId) fun onReportBugClicked() + fun onRoomDirectorySearchClicked() } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt index 5dec53e158..e9af66d331 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt @@ -64,10 +64,6 @@ class RoomListNode @AssistedInject constructor( plugins().forEach { it.onCreateRoomClicked() } } - private fun onSessionVerificationClicked() { - plugins().forEach { it.onSessionVerificationClicked() } - } - private fun onSessionConfirmRecoveryKeyClicked() { plugins().forEach { it.onSessionConfirmRecoveryKeyClicked() } } @@ -91,6 +87,10 @@ class RoomListNode @AssistedInject constructor( } } + private fun onRoomDirectorySearchClicked() { + plugins().forEach { it.onRoomDirectorySearchClicked() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -100,11 +100,11 @@ class RoomListNode @AssistedInject constructor( onRoomClicked = this::onRoomClicked, onSettingsClicked = this::onOpenSettings, onCreateRoomClicked = this::onCreateRoomClicked, - onVerifyClicked = this::onSessionVerificationClicked, onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked, onInvitesClicked = this::onInvitesClicked, onRoomSettingsClicked = this::onRoomSettingsClicked, onMenuActionClicked = { onMenuActionClicked(activity, it) }, + onRoomDirectorySearchClicked = this::onRoomDirectorySearchClicked, modifier = modifier, ) } 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 900e640981..0b5d07dc69 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 @@ -58,7 +58,6 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.collections.immutable.toPersistentList @@ -92,7 +91,6 @@ class RoomListPresenter @Inject constructor( private val analyticsService: AnalyticsService, ) : Presenter { private val encryptionService: EncryptionService = client.encryptionService() - private val sessionVerificationService: SessionVerificationService = client.sessionVerificationService() private val syncService: SyncService = client.syncService() @Composable @@ -159,19 +157,12 @@ class RoomListPresenter @Inject constructor( securityBannerDismissed: Boolean, ): State { val currentSecurityBannerDismissed by rememberUpdatedState(securityBannerDismissed) - val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) - val isLastDevice by encryptionService.isLastDevice.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val syncState by syncService.syncState.collectAsState() return remember { derivedStateOf { when { currentSecurityBannerDismissed -> SecurityBannerState.None - canVerifySession -> if (isLastDevice) { - SecurityBannerState.RecoveryKeyConfirmation - } else { - SecurityBannerState.SessionVerification - } recoveryState == RecoveryState.INCOMPLETE && syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation else -> SecurityBannerState.None 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 250f1c8e87..62f59b4eaa 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 @@ -63,7 +63,6 @@ enum class InvitesState { enum class SecurityBannerState { None, - SessionVerification, RecoveryKeyConfirmation, } 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 00f6833f02..c80430f9fe 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 @@ -45,7 +45,6 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.NewInvites)), aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")), aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)), - aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification)), aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)), aRoomListState(contentState = anEmptyContentState()), aRoomListState(contentState = aSkeletonContentState()), 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 23a105f143..881d06411e 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 @@ -53,12 +53,12 @@ fun RoomListView( state: RoomListState, onRoomClicked: (RoomId) -> Unit, onSettingsClicked: () -> Unit, - onVerifyClicked: () -> Unit, onConfirmRecoveryKeyClicked: () -> Unit, onCreateRoomClicked: () -> Unit, onInvitesClicked: () -> Unit, onRoomSettingsClicked: (roomId: RoomId) -> Unit, onMenuActionClicked: (RoomListMenuAction) -> Unit, + onRoomDirectorySearchClicked: () -> Unit, modifier: Modifier = Modifier, ) { ConnectivityIndicatorContainer( @@ -85,7 +85,6 @@ fun RoomListView( RoomListScaffold( modifier = Modifier.padding(top = topPadding), state = state, - onVerifyClicked = onVerifyClicked, onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onRoomClicked = onRoomClicked, onRoomLongClicked = { onRoomLongClicked(it) }, @@ -99,6 +98,7 @@ fun RoomListView( state = state.searchState, onRoomClicked = onRoomClicked, onRoomLongClicked = { onRoomLongClicked(it) }, + onRoomDirectorySearchClicked = onRoomDirectorySearchClicked, modifier = Modifier .statusBarsPadding() .padding(top = topPadding) @@ -113,7 +113,6 @@ fun RoomListView( @Composable private fun RoomListScaffold( state: RoomListState, - onVerifyClicked: () -> Unit, onConfirmRecoveryKeyClicked: () -> Unit, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, @@ -152,7 +151,6 @@ private fun RoomListScaffold( contentState = state.contentState, filtersState = state.filtersState, eventSink = state.eventSink, - onVerifyClicked = onVerifyClicked, onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onRoomClicked = ::onRoomClicked, onRoomLongClicked = onRoomLongClicked, @@ -191,11 +189,11 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class) state = state, onRoomClicked = {}, onSettingsClicked = {}, - onVerifyClicked = {}, onConfirmRecoveryKeyClicked = {}, onCreateRoomClicked = {}, onInvitesClicked = {}, onRoomSettingsClicked = {}, onMenuActionClicked = {}, + onRoomDirectorySearchClicked = {}, ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RequestVerificationHeader.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RequestVerificationHeader.kt deleted file mode 100644 index d3ad6db206..0000000000 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RequestVerificationHeader.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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 RequestVerificationHeader( - onVerifyClicked: () -> Unit, - onDismissClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - DialogLikeBannerMolecule( - modifier = modifier, - title = stringResource(R.string.session_verification_banner_title), - content = stringResource(R.string.session_verification_banner_message), - onSubmitClicked = onVerifyClicked, - onDismissClicked = onDismissClicked, - ) -} - -@PreviewsDayNight -@Composable -internal fun RequestVerificationHeaderPreview() = ElementPreview { - RequestVerificationHeader( - onVerifyClicked = {}, - onDismissClicked = {}, - ) -} 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 1bb5af4e7a..552ff008a0 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 @@ -73,7 +73,6 @@ fun RoomListContentView( contentState: RoomListContentState, filtersState: RoomListFiltersState, eventSink: (RoomListEvents) -> Unit, - onVerifyClicked: () -> Unit, onConfirmRecoveryKeyClicked: () -> Unit, onRoomClicked: (RoomListRoomSummary) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, @@ -103,7 +102,6 @@ fun RoomListContentView( state = contentState, filtersState = filtersState, eventSink = eventSink, - onVerifyClicked = onVerifyClicked, onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onRoomClicked = onRoomClicked, onRoomLongClicked = onRoomLongClicked, @@ -161,7 +159,6 @@ private fun RoomsView( state: RoomListContentState.Rooms, filtersState: RoomListFiltersState, eventSink: (RoomListEvents) -> Unit, - onVerifyClicked: () -> Unit, onConfirmRecoveryKeyClicked: () -> Unit, onRoomClicked: (RoomListRoomSummary) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, @@ -177,7 +174,6 @@ private fun RoomsView( RoomsViewList( state = state, eventSink = eventSink, - onVerifyClicked = onVerifyClicked, onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onRoomClicked = onRoomClicked, onRoomLongClicked = onRoomLongClicked, @@ -191,7 +187,6 @@ private fun RoomsView( private fun RoomsViewList( state: RoomListContentState.Rooms, eventSink: (RoomListEvents) -> Unit, - onVerifyClicked: () -> Unit, onConfirmRecoveryKeyClicked: () -> Unit, onRoomClicked: (RoomListRoomSummary) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, @@ -222,14 +217,6 @@ private fun RoomsViewList( contentPadding = PaddingValues(bottom = 80.dp) ) { when (state.securityBannerState) { - SecurityBannerState.SessionVerification -> { - item { - RequestVerificationHeader( - onVerifyClicked = onVerifyClicked, - onDismissClicked = { eventSink(RoomListEvents.DismissRequestVerificationPrompt) } - ) - } - } SecurityBannerState.RecoveryKeyConfirmation -> { item { ConfirmRecoveryKeyBanner( @@ -316,10 +303,10 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr filterSelectionStates = RoomListFilter.entries.map { FilterSelectionState(it, isSelected = true) } ), eventSink = {}, - onVerifyClicked = { }, - onConfirmRecoveryKeyClicked = { }, + onConfirmRecoveryKeyClicked = {}, onRoomClicked = {}, onRoomLongClicked = {}, - onCreateRoomClicked = { }, - onInvitesClicked = { }) + onCreateRoomClicked = {}, + onInvitesClicked = {} + ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt index 78bcda07f1..4ae7f091fe 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenter.kt @@ -21,21 +21,25 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import kotlinx.collections.immutable.persistentListOf import javax.inject.Inject class RoomListSearchPresenter @Inject constructor( private val dataSource: RoomListSearchDataSource, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): RoomListSearchState { - var isSearchActive by rememberSaveable { + // Do not use rememberSaveable so that search is not active when the user navigates back to the screen + var isSearchActive by remember { mutableStateOf(false) } - var searchQuery by rememberSaveable { + var searchQuery by remember { mutableStateOf("") } @@ -62,12 +66,14 @@ class RoomListSearchPresenter @Inject constructor( } } + val isRoomDirectorySearchEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch).collectAsState(initial = false) val searchResults by dataSource.roomSummaries.collectAsState(initial = persistentListOf()) return RoomListSearchState( isSearchActive = isSearchActive, query = searchQuery, results = searchResults, + isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, eventSink = ::handleEvents ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt index c4b24dc798..92e70ad039 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchState.kt @@ -23,5 +23,8 @@ data class RoomListSearchState( val isSearchActive: Boolean, val query: String, val results: ImmutableList, + val isRoomDirectorySearchEnabled: Boolean, val eventSink: (RoomListSearchEvents) -> Unit -) +) { + val displayRoomDirectorySearch = query.isEmpty() && isRoomDirectorySearchEnabled +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt index ae722a4b04..c4dcbab1ec 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchStateProvider.kt @@ -26,6 +26,7 @@ class RoomListSearchStateProvider : PreviewParameterProvider get() = sequenceOf( aRoomListSearchState(), + aRoomListSearchState(isRoomDirectorySearchEnabled = true), aRoomListSearchState( isSearchActive = true, query = "Test", @@ -38,10 +39,12 @@ fun aRoomListSearchState( isSearchActive: Boolean = false, query: String = "", results: ImmutableList = persistentListOf(), + isRoomDirectorySearchEnabled: Boolean = false, eventSink: (RoomListSearchEvents) -> Unit = { }, ) = RoomListSearchState( isSearchActive = isSearchActive, query = query, results = results, + isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, eventSink = eventSink, ) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt index eff6449449..80657ed4fc 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchView.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.roomlist.impl.R import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.contentType import io.element.android.features.roomlist.impl.model.RoomListRoomSummary @@ -50,8 +51,10 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.modifiers.applyIf 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.Icon import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -64,6 +67,7 @@ internal fun RoomListSearchView( state: RoomListSearchState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, + onRoomDirectorySearchClicked: () -> Unit, modifier: Modifier = Modifier, ) { BackHandler(enabled = state.isSearchActive) { @@ -87,6 +91,7 @@ internal fun RoomListSearchView( state = state, onRoomClicked = onRoomClicked, onRoomLongClicked = onRoomLongClicked, + onRoomDirectorySearchClicked = onRoomDirectorySearchClicked, ) } } @@ -99,6 +104,7 @@ private fun RoomListSearchContent( state: RoomListSearchState, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, + onRoomDirectorySearchClicked: () -> Unit, ) { val borderColor = MaterialTheme.colorScheme.tertiary val strokeWidth = 1.dp @@ -169,6 +175,14 @@ private fun RoomListSearchContent( .padding(padding) .consumeWindowInsets(padding) ) { + if (state.displayRoomDirectorySearch) { + RoomDirectorySearchButton( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 16.dp), + onClick = onRoomDirectorySearchClicked + ) + } LazyColumn( modifier = Modifier.weight(1f), ) { @@ -187,12 +201,26 @@ private fun RoomListSearchContent( } } +@Composable +private fun RoomDirectorySearchButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Button( + text = stringResource(id = R.string.screen_roomlist_room_directory_button_title), + leadingIcon = IconSource.Vector(CompoundIcons.ListBulleted()), + onClick = onClick, + modifier = modifier, + ) +} + @PreviewsDayNight @Composable internal fun RoomListSearchResultContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview { RoomListSearchContent( state = state, onRoomClicked = {}, - onRoomLongClicked = {} + onRoomLongClicked = {}, + onRoomDirectorySearchClicked = {}, ) } diff --git a/features/roomlist/impl/src/main/res/values-be/translations.xml b/features/roomlist/impl/src/main/res/values-be/translations.xml index e229e68f26..39cbd31bc6 100644 --- a/features/roomlist/impl/src/main/res/values-be/translations.xml +++ b/features/roomlist/impl/src/main/res/values-be/translations.xml @@ -1,7 +1,7 @@ "Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата." - "Пацвердзіце ключ аднаўлення" + "Увядзіце ключ аднаўлення" "Гэта аднаразовы працэс, дзякуем за чаканне." "Налада ўліковага запісу." "Стварыце новую размову або пакой" @@ -14,7 +14,7 @@ "Нізкі прыярытэт" "Вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты." "У вас няма чатаў для гэтай катэгорыі" - "Людзі" + "Удзельнікі" "У вас пакуль няма асабістых паведамленняў" "Пакоі" "Вас пакуль няма ў ніводным пакоі" diff --git a/features/roomlist/impl/src/main/res/values-hu/translations.xml b/features/roomlist/impl/src/main/res/values-hu/translations.xml index 16ba862467..559a481246 100644 --- a/features/roomlist/impl/src/main/res/values-hu/translations.xml +++ b/features/roomlist/impl/src/main/res/values-hu/translations.xml @@ -1,6 +1,6 @@ - "A csevegés biztonsági mentése nincs szinkronban. Meg kell erősítened a helyreállítási kulcsát, hogy továbbra is hozzáférj a csevegés biztonsági mentéséhez." + "A csevegés biztonsági mentése nincs szinkronban. Meg kell erősítenie a helyreállítási kulcsát, hogy továbbra is hozzáférjen a csevegés biztonsági mentéséhez." "Helyreállítási kulcs megerősítése" "Ez egy egyszeri folyamat, köszönjük a türelmét." "A fiók beállítása." @@ -24,6 +24,7 @@ Nincs olvasatlan üzenete!" "Összes csevegés" "Megjelölés olvasottként" "Megjelölés olvasatlanként" + "Összes szoba böngészése" "Úgy tűnik, hogy új eszközt használ. Ellenőrizze egy másik eszközzel, hogy a továbbiakban elérje a titkosított üzeneteket." "Ellenőrizze, hogy Ön az" diff --git a/features/roomlist/impl/src/main/res/values-in/translations.xml b/features/roomlist/impl/src/main/res/values-in/translations.xml index e84fd81fa7..74c90c1319 100644 --- a/features/roomlist/impl/src/main/res/values-in/translations.xml +++ b/features/roomlist/impl/src/main/res/values-in/translations.xml @@ -24,6 +24,7 @@ Anda tidak memiliki pesan yang belum dibaca!" "Semua Obrolan" "Tandai sebagai dibaca" "Tandai sebagai belum dibaca" + "Cari semua ruangan" "Sepertinya Anda menggunakan perangkat baru. Verifikasi dengan perangkat lain untuk mengakses pesan terenkripsi Anda selanjutnya." "Verifikasi bahwa ini Anda" diff --git a/features/roomlist/impl/src/main/res/values-uk/translations.xml b/features/roomlist/impl/src/main/res/values-uk/translations.xml index 3aaf84f9a5..d7ebacd5f7 100644 --- a/features/roomlist/impl/src/main/res/values-uk/translations.xml +++ b/features/roomlist/impl/src/main/res/values-uk/translations.xml @@ -15,7 +15,7 @@ "Ви можете зняти фільтри, щоб побачити інші ваші чати" "Ви не маєте чатів для цієї категорії" "Люди" - "У вас ще немає жодного особистого чату" + "Ви ще не маєте жодного особистого чату" "Кімнати" "Ви ще не учасник жодної кімнати" "Непрочитані" diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index 11466fdc56..deeead48c2 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -11,6 +11,8 @@ "You can add a chat to your favourites in the chat settings. For now, you can deselect filters in order to see your other chats" "You don’t have favourite chats yet" + "Invites" + "You don\'t have any pending invites." "Low Priority" "You can deselect filters in order to see your other chats" "You don’t have chats for this selection" diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 6b5877a010..0f2c288a63 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -239,52 +239,28 @@ class RoomListPresenterTests { } } - @Test - fun `present - handle RecoveryKeyConfirmation last session`() = runTest { - val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) - val roomListService = FakeRoomListService().apply { - postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) - } - val presenter = createRoomListPresenter( - coroutineScope = scope, - client = FakeMatrixClient( - encryptionService = FakeEncryptionService().apply { - emitIsLastDevice(true) - }, - roomListService = roomListService - ), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val eventSink = consumeItemsUntilPredicate { - it.contentState is RoomListContentState.Rooms - }.last().eventSink - // For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation - assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) - eventSink(RoomListEvents.DismissRequestVerificationPrompt) - assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) - scope.cancel() - } - } - @Test fun `present - handle DismissRequestVerificationPrompt`() = runTest { val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val roomListService = FakeRoomListService().apply { postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) } + val encryptionService = FakeEncryptionService().apply { + emitRecoveryState(RecoveryState.INCOMPLETE) + } + val syncService = FakeSyncService(initialState = SyncState.Running) val presenter = createRoomListPresenter( - client = FakeMatrixClient(roomListService = roomListService), + client = FakeMatrixClient(roomListService = roomListService, encryptionService = encryptionService, syncService = syncService), coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val eventSink = consumeItemsUntilPredicate { + val eventWithContentAsRooms = consumeItemsUntilPredicate { it.contentState is RoomListContentState.Rooms - }.last().eventSink - assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification) + }.last() + val eventSink = eventWithContentAsRooms.eventSink + assertThat(eventWithContentAsRooms.contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) eventSink(RoomListEvents.DismissRequestVerificationPrompt) assertThat(awaitItem().contentAsRooms().securityBannerState).isEqualTo(SecurityBannerState.None) scope.cancel() @@ -342,10 +318,10 @@ class RoomListPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - consumeItemsUntilPredicate { + val firstItem = consumeItemsUntilPredicate { it.contentState is RoomListContentState.Rooms - } - assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites) + }.last() + assertThat(firstItem.contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites) inviteStateFlow.value = InvitesState.SeenInvites assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites) 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 f545b27860..2dbec36395 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 @@ -43,35 +43,6 @@ import org.junit.runner.RunWith class RoomListViewTest { @get:Rule val rule = createAndroidComposeRule() - @Test - fun `clicking on close verification banner emits the expected Event`() { - val eventsRecorder = EventsRecorder() - rule.setRoomListView( - state = aRoomListState( - contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification), - eventSink = eventsRecorder, - ) - ) - val close = rule.activity.getString(CommonStrings.action_close) - rule.onNodeWithContentDescription(close).performClick() - eventsRecorder.assertSingle(RoomListEvents.DismissRequestVerificationPrompt) - } - - @Test - fun `clicking on continue verification banner invokes the expected callback`() { - val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnce { callback -> - rule.setRoomListView( - state = aRoomListState( - contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SessionVerification), - eventSink = eventsRecorder, - ), - onVerifyClicked = callback, - ) - rule.clickOn(CommonStrings.action_continue) - } - } - @Test fun `clicking on close recovery key banner emits the expected Event`() { val eventsRecorder = EventsRecorder() @@ -185,24 +156,24 @@ private fun AndroidComposeTestRule.setRoomL state: RoomListState, onRoomClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onSettingsClicked: () -> Unit = EnsureNeverCalled(), - onVerifyClicked: () -> Unit = EnsureNeverCalled(), onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(), onCreateRoomClicked: () -> Unit = EnsureNeverCalled(), onInvitesClicked: () -> Unit = EnsureNeverCalled(), onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), + onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(), ) { setContent { RoomListView( state = state, onRoomClicked = onRoomClicked, onSettingsClicked = onSettingsClicked, - onVerifyClicked = onVerifyClicked, onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onCreateRoomClicked = onCreateRoomClicked, onInvitesClicked = onInvitesClicked, onRoomSettingsClicked = onRoomSettingsClicked, onMenuActionClicked = onMenuActionClicked, + onRoomDirectorySearchClicked = onRoomDirectorySearchClicked, ) } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt index d3fc434f25..b3463c549f 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTests.kt @@ -23,6 +23,9 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary @@ -128,10 +131,26 @@ class RoomListSearchPresenterTests { } } } + + @Test + fun `present - room directory search`() = runTest { + val featureFlagService = FakeFeatureFlagService() + featureFlagService.setFeatureEnabled(FeatureFlags.RoomDirectorySearch, true) + val presenter = createRoomListSearchPresenter(featureFlagService = featureFlagService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + awaitItem().let { state -> + assertThat(state.isRoomDirectorySearchEnabled).isTrue() + } + } + } } fun TestScope.createRoomListSearchPresenter( roomListService: RoomListService = FakeRoomListService(), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), ): RoomListSearchPresenter { return RoomListSearchPresenter( dataSource = RoomListSearchDataSource( @@ -141,6 +160,7 @@ fun TestScope.createRoomListSearchPresenter( roomLastMessageFormatter = FakeRoomLastMessageFormatter(), ), coroutineDispatchers = testCoroutineDispatchers(), - ) + ), + featureFlagService = featureFlagService, ) } diff --git a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt index 1fee6418a9..1904ceb2ff 100644 --- a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt +++ b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt @@ -19,6 +19,7 @@ package io.element.android.features.securebackup.api import android.os.Parcelable import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs import kotlinx.parcelize.Parcelize @@ -30,14 +31,23 @@ interface SecureBackupEntryPoint : FeatureEntryPoint { @Parcelize data object EnterRecoveryKey : InitialTarget + + @Parcelize + data object CreateNewRecoveryKey : InitialTarget } data class Params(val initialElement: InitialTarget) : NodeInputs fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + interface Callback : Plugin { + fun onCreateNewRecoveryKey() + fun onDone() + } + interface NodeBuilder { fun params(params: Params): NodeBuilder + fun callback(callback: Callback): NodeBuilder fun build(): Node } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt index e3d5fde961..d238560463 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt @@ -36,6 +36,11 @@ class DefaultSecureBackupEntryPoint @Inject constructor() : SecureBackupEntryPoi return this } + override fun callback(callback: SecureBackupEntryPoint.Callback): SecureBackupEntryPoint.NodeBuilder { + plugins += callback + return this + } + override fun build(): Node { return parentNode.createNode(buildContext, plugins) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt index 172c6672e5..1f22e57c03 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt @@ -22,12 +22,15 @@ 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 com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.securebackup.api.SecureBackupEntryPoint +import io.element.android.features.securebackup.impl.createkey.CreateNewRecoveryKeyNode import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode @@ -48,6 +51,7 @@ class SecureBackupFlowNode @AssistedInject constructor( initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) { SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey + SecureBackupEntryPoint.InitialTarget.CreateNewRecoveryKey -> NavTarget.CreateNewRecoveryKey }, savedStateMap = buildContext.savedStateMap, ), @@ -72,8 +76,13 @@ class SecureBackupFlowNode @AssistedInject constructor( @Parcelize data object EnterRecoveryKey : NavTarget + + @Parcelize + data object CreateNewRecoveryKey : NavTarget } + private val callback = plugins().firstOrNull() + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { @@ -119,7 +128,19 @@ class SecureBackupFlowNode @AssistedInject constructor( createNode(buildContext) } NavTarget.EnterRecoveryKey -> { - createNode(buildContext) + val callback = object : SecureBackupEnterRecoveryKeyNode.Callback { + override fun onEnterRecoveryKeySuccess() { + if (callback != null) { + callback.onDone() + } else { + backstack.pop() + } + } + } + createNode(buildContext, plugins = listOf(callback)) + } + NavTarget.CreateNewRecoveryKey -> { + createNode(buildContext) } } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyNode.kt new file mode 100644 index 0000000000..df1e2d9528 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyNode.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.createkey + +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 dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class CreateNewRecoveryKeyNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext, plugins = plugins) { + @Composable + override fun View(modifier: Modifier) { + CreateNewRecoveryKeyView( + modifier = modifier, + onBackClicked = ::navigateUp, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt new file mode 100644 index 0000000000..ed3a5cd339 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/createkey/CreateNewRecoveryKeyView.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.securebackup.impl.createkey + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +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.securebackup.impl.R +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.PageTitle +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.modifiers.squareSize +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateNewRecoveryKeyView( + onBackClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar(title = {}, navigationIcon = { BackButton(onClick = onBackClicked) }) + } + ) { padding -> + Column( + modifier = Modifier.padding(padding) + ) { + PageTitle( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp), + title = stringResource(R.string.screen_create_new_recovery_key_title), + iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()) + ) + Content() + } + } +} + +@Composable +private fun Content() { + Column(modifier = Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) { + Item(index = 1, text = AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_1))) + Item(index = 2, text = AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_2))) + Item( + index = 3, + text = buildAnnotatedString { + val resetAllAction = stringResource(R.string.screen_create_new_recovery_key_list_item_3_reset_all) + val text = stringResource(R.string.screen_create_new_recovery_key_list_item_3, resetAllAction) + append(text) + val start = text.indexOf(resetAllAction) + val end = start + resetAllAction.length + if (start in text.indices && end in text.indices) { + addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) + } + } + ) + Item(index = 4, text = AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_4))) + Item(index = 5, text = AnnotatedString(stringResource(R.string.screen_create_new_recovery_key_list_item_5))) + } +} + +@Composable +private fun Item(index: Int, text: AnnotatedString) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + ItemNumber(index = index) + Text(text = text, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textPrimary) + } +} + +@Composable +private fun ItemNumber( + index: Int, +) { + val color = ElementTheme.colors.textPlaceholder + Box( + modifier = Modifier + .border(1.dp, color, CircleShape) + .squareSize() + ) { + Text( + modifier = Modifier.padding(1.5.dp), + text = index.toString(), + style = ElementTheme.typography.fontBodySmRegular, + color = color, + textAlign = TextAlign.Center, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun CreateNewRecoveryKeyViewPreview() { + ElementPreview { + CreateNewRecoveryKeyView( + onBackClicked = {}, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt index 8c6ae282fa..597ab4c14e 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt @@ -17,48 +17,36 @@ package io.element.android.features.securebackup.impl.enter import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope 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.securebackup.impl.R -import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.di.SessionScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch @ContributesNode(SessionScope::class) class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val presenter: SecureBackupEnterRecoveryKeyPresenter, - private val snackbarDispatcher: SnackbarDispatcher, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onEnterRecoveryKeySuccess() + } + + private val callback = plugins().first() + @Composable override fun View(modifier: Modifier) { - val coroutineScope = rememberCoroutineScope() val state = presenter.present() SecureBackupEnterRecoveryKeyView( state = state, modifier = modifier, - onDone = { - coroutineScope.postSuccessSnackbar() - navigateUp() - }, + onDone = callback::onEnterRecoveryKeySuccess, onBackClicked = ::navigateUp, ) } - - private fun CoroutineScope.postSuccessSnackbar() = launch { - snackbarDispatcher.post( - SnackbarMessage( - messageResId = R.string.screen_recovery_key_confirm_success - ) - ) - } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt index ad36c09726..b74078c7f2 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt @@ -83,7 +83,7 @@ private fun ColumnScope.Buttons( state: SecureBackupEnterRecoveryKeyState, ) { Button( - text = stringResource(id = CommonStrings.action_confirm), + text = stringResource(id = CommonStrings.action_continue), enabled = state.isSubmitEnabled, showProgress = state.submitAction.isLoading(), modifier = Modifier.fillMaxWidth(), diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt index 7a4d92fcd3..dc8b788025 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt @@ -70,7 +70,10 @@ internal fun RecoveryKeyView( verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = stringResource(id = CommonStrings.common_recovery_key), + text = when (state.recoveryKeyUserStory) { + RecoveryKeyUserStory.Enter -> stringResource(R.string.screen_recovery_key_confirm_key_label) + else -> stringResource(id = CommonStrings.common_recovery_key) + }, modifier = Modifier.padding(start = 16.dp), style = ElementTheme.typography.fontBodyMdRegular, ) diff --git a/features/securebackup/impl/src/main/res/values-be/translations.xml b/features/securebackup/impl/src/main/res/values-be/translations.xml index d16399978d..58011bad6e 100644 --- a/features/securebackup/impl/src/main/res/values-be/translations.xml +++ b/features/securebackup/impl/src/main/res/values-be/translations.xml @@ -5,29 +5,38 @@ "Рэзервовае капіраванне гарантуе, што вы не страціце сваю гісторыю паведамленняў. %1$s." "Рэзервовая копія" "Змяніць ключ аднаўлення" - "Пацвердзіце ключ аднаўлення" + "Увядзіце ключ аднаўлення" "Ваша рэзервовая копія чата зараз не сінхранізавана." "Наладзьце аднаўленне" "Атрымайце доступ да зашыфраваных паведамленняў, калі вы страціце ўсе свае прылады або выйдзеце з сістэмы %1$s усюды." + "Адкрыйце Element на настольнай прыладзе" + "Увайдзіце ў свой уліковы запіс яшчэ раз" + "Калі будзе прапанавана пацвердзіць вашу прыладу, выберыце %1$s" + "“Скінуць усе”" + "Выконвайце інструкцыі, каб стварыць новы ключ аднаўлення" + "Захавайце новы ключ аднаўлення ў ме́неджэры пароляў або ў зашыфраванай нататке" + "Скіньце шыфраванне для вашага ўліковага запісу з дапамогай іншай прылады" "Адключыць" "Вы страціце зашыфраваныя паведамленні, калі выйдзеце з усіх прылад." - "Вы ўпэўнены, што жадаеце адключыць рэзервовае капіраванне?" + "Вы ўпэўнены, што хочаце адключыць рэзервовае капіраванне?" "Адключэнне рэзервовага капіравання прывядзе да выдалення бягучай рэзервовай копіі ключа шыфравання і адключэння іншых функцый бяспекі. У гэтым выпадку вы:" "Няма зашыфраванай гісторыі паведамленняў на новых прыладах" "Калі вы выходзіце з сістэмы, то губляеце доступ да зашыфраваных паведамленняў %1$s усюды" - "Вы ўпэўнены, што жадаеце адключыць рэзервовае капіраванне?" + "Вы ўпэўнены, што хочаце адключыць рэзервовае капіраванне?" "Атрымайце новы ключ аднаўлення, калі вы страцілі існуючы. Пасля змены ключа аднаўлення ваш стары больш не будзе працаваць." "Стварыць новы ключ аднаўлення" "Пераканайцеся, што вы можаце захаваць ключ аднаўлення ў бяспечным месцы" "Ключ аднаўлення зменены" "Змяніць ключ аднаўлення?" - "Увядзіце ключ аднаўлення, каб пацвердзіць доступ да рэзервовай копіі чата." + "Стварыць новы ключ аднаўлення" + "Пераканайцеся, што ніхто не бачыць гэты экран!" "Паўтарыце спробу, каб пацвердзіць доступ да рэзервовай копіі чата." "Няправільны ключ аднаўлення" - "Увядзіце код з 48 сімвалаў." + "Калі ў вас ёсць ключ аднаўлення або парольная фраза, гэта таксама будзе працаваць." + "Ключ аднаўлення або код доступу" "Увесці…" "Ключ аднаўлення пацверджаны" - "Пацвердзіце ключ аднаўлення" + "Увядзіце ключ аднаўлення або код доступу" "Ключ аднаўлення скапіраваны" "Стварэнне…" "Захаваць ключ аднаўлення" diff --git a/features/securebackup/impl/src/main/res/values-cs/translations.xml b/features/securebackup/impl/src/main/res/values-cs/translations.xml index a34d77aeee..f8a2fe8747 100644 --- a/features/securebackup/impl/src/main/res/values-cs/translations.xml +++ b/features/securebackup/impl/src/main/res/values-cs/translations.xml @@ -9,6 +9,13 @@ "Vaše záloha chatu není aktuálně synchronizována." "Nastavení obnovy" "Získejte přístup ke svým zašifrovaným zprávám, pokud ztratíte všechna zařízení nebo jste všude odhlášeni z %1$s." + "Otevřít Element na stolním počítači" + "Znovu se přihlaste ke svému účtu" + "Když budete vyzváni k ověření vašeho zařízení, vyberte %1$s" + "\"Resetovat vše\"" + "Postupujte podle pokynů k vytvoření nového obnovovacího klíče" + "Uložte nový klíč pro obnovení do správce hesel nebo do zašifrované poznámky" + "Obnovte šifrování účtu pomocí jiného zařízení" "Vypnout" "Pokud se odhlásíte ze všech zařízení, přijdete o zašifrované zprávy." "Opravdu chcete vypnout zálohování?" @@ -21,10 +28,12 @@ "Ujistěte se, že můžete klíč pro obnovení uložit někde v bezpečí" "Klíč pro obnovení byl změněn" "Změnit klíč pro obnovení?" - "Zadejte klíč pro obnovení a potvrďte přístup k záloze chatu." + "Vytvořit nový klíč pro obnovení" + "Ujistěte se, že tuto obrazovku nikdo nevidí!" "Zkuste prosím znovu potvrdit přístup k záloze chatu." "Nesprávný klíč pro obnovení" - "Zadejte kód o délce 48 znaků." + "Pokud máte bezpečnostní klíč nebo bezpečnostní frázi, bude to fungovat také." + "Klíč pro obnovení nebo přístupový kód" "Zadejte…" "Klíč pro obnovení potvrzen" "Potvrďte klíč pro obnovení" diff --git a/features/securebackup/impl/src/main/res/values-de/translations.xml b/features/securebackup/impl/src/main/res/values-de/translations.xml index 5ec8f99e8c..7336b28b70 100644 --- a/features/securebackup/impl/src/main/res/values-de/translations.xml +++ b/features/securebackup/impl/src/main/res/values-de/translations.xml @@ -9,6 +9,26 @@ "Dein Chat-Backup ist derzeit nicht synchronisiert." "Wiederherstellung einrichten" "Erhalte Zugriff auf deine verschlüsselten Nachrichten, wenn du alle deine Geräte verlierst oder von %1$s überall abgemeldet bist." + + "Öffne " + "Element" + " auf einem " + "Desktop-Gerät" + + "Melde dich erneut bei deinem Konto an" + "Wenn du aufgefordert wirst dein Gerät zu verifizieren, wähle \"%1$s\"." + "Alles zurücksetzen" + "Folge den Anweisungen, um einen neuen Wiederherstellungsschlüssel zu erstellen" + + "Speichere deinen neuen " + "Wiederherstellungsschlüssel" + " in einem Passwortmanager oder einer verschlüsselten Notiz" + + + "Erstelle einen neuen " + "Wiederherstellungsschlüssel" + " mit einem anderen Gerät" + "Ausschalten" "Du verlierst deine verschlüsselten Nachrichten, wenn du auf allen Geräten abgemeldet bist." "Bist du sicher, dass du das Backup ausschalten willst?" @@ -21,13 +41,22 @@ "Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst" "Wiederherstellungsschlüssel geändert" "Wiederherstellungsschlüssel ändern?" - "Gib deinen Wiederherstellungsschlüssel ein, um den Zugriff auf dein Chat-Backup zu bestätigen." + + "Neuen " + "Wiederherstellungsschlüssel" + " erstellen" + + "Sorge dafür, dass niemand diesen Bildschirm sehen kann!" "Bitte versuche es noch einmal, um den Zugriff auf dein Chat-Backup zu bestätigen." "Falscher Wiederherstellungsschlüssel" - "Gib den 48-stelligen Code ein." + "Dies funktioniert auch mit einem Sicherheitsschlüssel oder Sicherheitsphrase." + + "Wiederherstellungsschlüssel" + " oder Passcode" + "Eingeben…" "Wiederherstellungsschlüssel bestätigt" - "Wiederherstellungsschlüssel bestätigen." + "Wiederherstellungsschlüssel oder Passcode bestätigen" "Wiederherstellungsschlüssel kopiert" "Generieren…" "Wiederherstellungsschlüssel speichern" diff --git a/features/securebackup/impl/src/main/res/values-hu/translations.xml b/features/securebackup/impl/src/main/res/values-hu/translations.xml index 0eff260322..1bd37c4b64 100644 --- a/features/securebackup/impl/src/main/res/values-hu/translations.xml +++ b/features/securebackup/impl/src/main/res/values-hu/translations.xml @@ -9,6 +9,13 @@ "A csevegéselőzményei nincsenek szinkronban." "Helyreállítás beállítása" "Szerezzen hozzáférést a titkosított üzeneteihez, ha elvesztette az összes eszközét, vagy ha mindenütt kijelentkezett az %1$sből." + "Nyissa meg az Elementet egy asztali eszközön" + "Jelentkezzen be újra a fiókjába" + "Amikor az eszköz ellenőrzését kéri, válassza ezt a lehetőséget: %1$s" + "„Minden visszaállítása”" + "Kövesse az utasításokat egy új helyreállítási kulcs létrehozásához" + "Mentse az új helyreállítási kulcsot egy jelszókezelőbe vagy egy titkosított jegyzetbe." + "A fiók titkosításának visszaállítása egy másik eszköz használatával" "Kikapcsolás" "Ha kijelentkezik az összes eszközéről, akkor elveszti a titkosított üzeneteit." "Biztos, hogy kikapcsolja a biztonsági mentéseket?" @@ -21,13 +28,15 @@ "Gondoskodjon arról, hogy biztonságos helyen tárolja a helyreállítási kulcsát" "Helyreállítási kulcs lecserélve" "Módosítja a helyreállítási kulcsot?" - "Adja meg a helyreállítási kulcsát, hogy megerősítse a csevegések biztonsági mentéséhez való hozzáférését." + "Új helyreállítási kulcs létrehozása" + "Győződjön meg arról, hogy senki sem látja ezt a képernyőt!" "Próbálja meg újra megerősíteni a csevegés biztonsági mentéséhez való hozzáférését." "Helytelen helyreállítási kulcs" - "Adja meg a 48 karakteres kódot." + "Ha van helyreállítási kulcsa vagy titkos jelmondata/kulcsa, akkor ez is fog működni." + "Helyreállítási kulcs vagy jelkód" "Megadás…" "Helyreállítási kulcs megerősítve" - "Erősítse meg a helyreállítási kulcsát" + "Adja meg a helyreállítási kulcsát vagy a jelkódját" "Helyreállítási kulcs másolva" "Előállítás…" "Helyreállítási kulcs mentése" diff --git a/features/securebackup/impl/src/main/res/values-in/translations.xml b/features/securebackup/impl/src/main/res/values-in/translations.xml index c7712832d2..016d0e3057 100644 --- a/features/securebackup/impl/src/main/res/values-in/translations.xml +++ b/features/securebackup/impl/src/main/res/values-in/translations.xml @@ -9,6 +9,13 @@ "Pencadangan percakapan Anda saat ini tidak tersinkron." "Siapkan pemulihan" "Dapatkan akses ke pesan terenkripsi Anda jika Anda kehilangan semua perangkat Anda atau keluar dari %1$s di mana pun." + "Buka Element di perangkat desktop" + "Masuk ke akun Anda lagi" + "Saat diminta untuk memverifikasi perangkat Anda, pilih %1$s" + "“Atur ulang semua”" + "Ikuti petunjuk untuk membuat kunci pemulihan baru" + "Simpan kunci pemulihan baru Anda dalam pengelola kata sandi atau catatan terenkripsi" + "Atur ulang enkripsi untuk akun Anda menggunakan perangkat lain" "Matikan" "Anda akan kehilangan pesan terenkripsi jika Anda keluar dari semua perangkat." "Apakah Anda yakin ingin mematikan pencadangan?" @@ -21,13 +28,15 @@ "Pastikan Anda dapat menyimpan kunci pemulihan Anda di tempat yang aman" "Kunci pemulihan diganti" "Ubah kunci pemulihan?" - "Masukkan kunci pemulihan Anda untuk mengonfirmasi akses ke cadangan percakapan Anda." + "Buat kunci pemulihan baru" + "Pastikan tidak ada yang bisa melihat layar ini!" "Silakan coba lagi untuk mengonfirmasi akses ke cadangan percakapan Anda." "Kunci pemulihan salah" - "Masukkan kode 48 karakter." + "Jika Anda memiliki frasa sandi pemulihan atau frasa/kunci sandi rahasia, ini juga dapat digunakan." + "Kunci pemulihan atau kode sandi" "Masukkan…" "Kunci pemulihan dikonfirmasi" - "Konfirmasi kunci pemulihan Anda" + "Konfirmasi kunci pemulihan atau kode sandi Anda" "Kunci pemulihan disalin" "Membuat…" "Simpan kunci pemulihan" diff --git a/features/securebackup/impl/src/main/res/values-ru/translations.xml b/features/securebackup/impl/src/main/res/values-ru/translations.xml index fd2bb28208..acacc8d60b 100644 --- a/features/securebackup/impl/src/main/res/values-ru/translations.xml +++ b/features/securebackup/impl/src/main/res/values-ru/translations.xml @@ -4,11 +4,28 @@ "Включить резервное копирование" "Резервное копирование гарантирует, что вы не потеряете историю сообщений. %1$s." "Резервное копирование" - "Изменить ключ восстановления" - "Подтвердить ключ восстановления" + + "Изменить " + "ключ восстановления" + + + "Ввести " + "ключ восстановления" + "Резервная копия чата в настоящее время не синхронизирована." "Настроить восстановление" "Получите доступ к зашифрованным сообщениям, если вы потеряете все свои устройства или выйдете из системы %1$s отовсюду." + "Откройте Element на настольном устройстве" + "Войдите в свой аккаунт еще раз" + "Когда вас попросят подтвердить устройство, выберите %1$s" + "“Сбросить все”" + "Следуйте инструкциям, чтобы создать новый ключ восстановления" + + "Сохраните новый " + "ключ восстановления" + " в менеджере паролей или зашифрованной заметке" + + "Сбросьте шифрование вашей учетной записи с помощью другого устройства." "Выключить" "Вы потеряете зашифрованные сообщения, если выйдете из всех устройств." "Вы действительно хотите отключить резервное копирование?" @@ -17,27 +34,62 @@ "Потерять доступ к зашифрованным сообщениям, если вы вышли из %1$s любой точки мира" "Вы действительно хотите отключить резервное копирование?" "Получите новый ключ восстановления, если вы потеряли существующий. После смены ключа восстановления старый ключ больше не будет работать." - "Создать новый ключ восстановления" + + "Создать новый " + "ключ восстановления" + "Убедитесь, что вы можете хранить ключ восстановления в безопасном месте" - "Ключ восстановления изменен" + + "Ключ восстановления" + " изменен" + "Изменить ключ восстановления?" - "Введите ключ восстановления, чтобы подтвердить доступ к резервной копии чата." + + "Создать новый " + "ключ восстановления" + + "Убедитесь, что никто не видит этот экран!" "Пожалуйста, попробуйте еще раз, чтобы подтвердить доступ к резервной копии чата." - "Неверный ключ восстановления" - "Введите 48 значный код." + + "Неверный " + "ключ восстановления" + + "Если у вас есть пароль для восстановления или секретный пароль/ключ, это тоже сработает." + + "Ключ восстановления" + " или пароль" + "Вход…" - "Ключ восстановления подтвержден" - "Подтвердите ключ восстановления" - "Ключ восстановления скопирован" + + "Ключ восстановления" + " подтвержден" + + + "Подтвердите " + "ключ восстановления" + + + "Ключ восстановления" + " скопирован" + "Генерация…" - "Сохранить ключ восстановления" + + "Сохранить " + "ключ восстановления" + "Запишите ключ восстановления в безопасном месте или сохраните его в менеджере паролей." "Нажмите, чтобы скопировать ключ восстановления" - "Сохраните ключ восстановления" + + "Сохраните " + "ключ восстановления" + "После этого шага вы не сможете получить доступ к новому ключу восстановления." "Вы сохранили ключ восстановления?" "Резервная копия чата защищена ключом восстановления. Если после настройки вам понадобится новый ключ восстановления, вы можете создать его заново, выбрав «Изменить ключ восстановления»." - "Сгенерируйте свой ключ восстановления" + + "Создайте " + "ключ восстановления" + "Убедитесь, что вы можете хранить ключ восстановления в безопасном месте" "Настройка восстановления выполнена успешно" "Настроить восстановление" diff --git a/features/securebackup/impl/src/main/res/values-sk/translations.xml b/features/securebackup/impl/src/main/res/values-sk/translations.xml index ffc7e78c87..fdc70027b3 100644 --- a/features/securebackup/impl/src/main/res/values-sk/translations.xml +++ b/features/securebackup/impl/src/main/res/values-sk/translations.xml @@ -9,6 +9,13 @@ "Vaša záloha konverzácie nie je momentálne synchronizovaná." "Nastaviť obnovovanie" "Získajte prístup k vašim šifrovaným správam aj keď stratíte všetky svoje zariadenia alebo sa odhlásite zo všetkých %1$s zariadení." + "Otvoriť Element v stolnom počítači" + "Znova sa prihláste do svojho účtu" + "Keď sa zobrazí výzva na overenie vášho zariadenia, vyberte %1$s" + "\"Obnoviť všetko\"" + "Postupujte podľa pokynov na vytvorenie nového kľúča na obnovenie" + "Uložte si nový kľúč na obnovenie do správcu hesiel alebo do zašifrovanej poznámky" + "Obnovte šifrovanie vášho účtu pomocou iného zariadenia" "Vypnúť" "Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite zo všetkých zariadení" "Ste si istí, že chcete vypnúť zálohovanie?" @@ -21,13 +28,15 @@ "Uistite sa, že kľúč na obnovenie môžete uložiť niekde v bezpečí" "Kľúč na obnovenie bol zmenený" "Zmeniť kľúč na obnovenie?" - "Zadajte kľúč na obnovenie a potvrďte prístup k zálohe konverzácie." + "Vytvoriť nový kľúč na obnovenie" + "Uistite sa, že túto obrazovku nikto nevidí!" "Skúste prosím znova potvrdiť prístup k vašej zálohe konverzácie." "Nesprávny kľúč na obnovenie" - "Zadajte 48-znakový kód." + "Ak máte frázu na obnovenie alebo tajné heslo/kľúč, bude to tiež fungovať." + "Kľúč na obnovenie alebo prístupový kód" "Zadať…" "Kľúč na obnovu potvrdený" - "Potvrďte kľúč na obnovenie" + "Zadajte kľúč na obnovenie alebo prístupový kód" "Skopírovaný kľúč na obnovenie" "Generovanie…" "Uložiť kľúč na obnovenie" diff --git a/features/securebackup/impl/src/main/res/values-sv/translations.xml b/features/securebackup/impl/src/main/res/values-sv/translations.xml index db8306b302..99c9c97b8a 100644 --- a/features/securebackup/impl/src/main/res/values-sv/translations.xml +++ b/features/securebackup/impl/src/main/res/values-sv/translations.xml @@ -4,5 +4,37 @@ "Slå på säkerhetskopiering" "Säkerhetskopior ser till att du inte blir av med din meddelandehistorik. %1$s." "Säkerhetskopia" + "Byt återställningsnyckel" + "Ange återställningsnyckel" + "Din chattsäkerhetskopia är för närvarande osynkroniserad." "Ställ in återställning" + "Få tillgång till dina krypterade meddelanden om du tappar bort alla dina enheter eller blir utloggad ur %1$s överallt." + "Stäng av" + "Du kommer att förlora dina krypterade meddelanden om du loggas ut från alla enheter." + "Är du säker på att du vill stänga av säkerhetskopiering?" + "Om du stänger av säkerhetskopiering tas din nuvarande säkerhetskopiering av krypteringsnycklar bort och andra säkerhetsfunktioner stängs av. I det här fallet kommer du att:" + "Inte ha krypterad meddelandehistorik på nya enheter" + "Förlora åtkomsten till dina krypterade meddelanden om du loggas ut ur %1$s överallt" + "Är du säker på att du vill stänga av säkerhetskopiering?" + "Få en ny återställningsnyckel om du har tappat bort din befintliga. När du har bytt din återställningsnyckel fungerar din gamla inte längre." + "Generera en ny återställningsnyckel" + "Se till att du kan lagra din återställningsnyckel någonstans säkert" + "Återställningsnyckel ändrad" + "Byt återställningsnyckel?" + "Ange din återställningsnyckel för att bekräfta åtkomst till din chattsäkerhetskopia." + "Ange koden på 48 tecken." + "Ange …" + "Återställningsnyckel bekräftad" + "Ange din återställningsnyckel" + "Spara återställningsnyckeln" + "Skriv ner din återställningsnyckel någonstans säkert eller spara den i en lösenordshanterare." + "Tryck för att kopiera återställningsnyckeln" + "Spara din återställningsnyckel" + "Du kommer inte att kunna komma åt din nya återställningsnyckel efter det här steget." + "Har du sparat din återställningsnyckel?" + "Din chattsäkerhetskopia skyddas av en återställningsnyckel. Om du behöver en ny återställningsnyckel efter installationen kan du återskapa genom att välja ”Byt återställningsnyckel”." + "Generera din återställningsnyckel" + "Se till att du kan lagra din återställningsnyckel någonstans säkert" + "Konfiguration av återställning lyckades" + "Ställ in återställning" diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml index f05725a075..9e188d1251 100644 --- a/features/securebackup/impl/src/main/res/values/localazy.xml +++ b/features/securebackup/impl/src/main/res/values/localazy.xml @@ -9,6 +9,13 @@ "Your chat backup is currently out of sync." "Set up recovery" "Get access to your encrypted messages if you lose all your devices or are signed out of %1$s everywhere." + "Open Element in a desktop device" + "Sign into your account again" + "When asked to verify your device, select %1$s" + "“Reset all”" + "Follow the instructions to create a new recovery key" + "Save your new recovery key in a password manager or encrypted note" + "Reset the encryption for your account using another device" "Turn off" "You will lose your encrypted messages if you are signed out of all devices." "Are you sure you want to turn off backup?" @@ -21,13 +28,15 @@ "Make sure you can store your recovery key somewhere safe" "Recovery key changed" "Change recovery key?" - "Enter your recovery key to confirm access to your chat backup." + "Create new recovery key" + "Make sure nobody can see this screen!" "Please try again to confirm access to your chat backup." "Incorrect recovery key" - "Enter the 48 character code." + "If you have a security key or security phrase, this will work too." + "Recovery key or passcode" "Enter…" "Recovery key confirmed" - "Enter your recovery key" + "Enter your recovery key or passcode" "Copied recovery key" "Generating…" "Save recovery key" diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt index df3549b0f8..9827d8d720 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -38,6 +38,7 @@ fun aSignedOutState() = SignedOutState( fun aSessionData( sessionId: SessionId = SessionId("@alice:server.org"), isTokenValid: Boolean = false, + needsVerification: Boolean = false, ): SessionData { return SessionData( userId = sessionId.value, @@ -51,5 +52,6 @@ fun aSessionData( isTokenValid = isTokenValid, loginType = LoginType.UNKNOWN, passphrase = null, + needsVerification = needsVerification, ) } diff --git a/features/signedout/impl/src/main/res/values-be/translations.xml b/features/signedout/impl/src/main/res/values-be/translations.xml index 42cb5e41ff..d95aa0df81 100644 --- a/features/signedout/impl/src/main/res/values-be/translations.xml +++ b/features/signedout/impl/src/main/res/values-be/translations.xml @@ -1,6 +1,6 @@ - "Вы змянілі свой пароль на іншым сеансе" + "Вы змянілі свой пароль у іншым сеансе" "Вы выдалілі сеанс з іншага сеансу" "Адміністратар вашага сервера ануляваў ваш доступ" "Магчыма, вы выйшлі з сістэмы па адной з прычын, пералічаных ніжэй. Калі ласка, увайдзіце яшчэ раз, каб працягнуць выкарыстанне %s." diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt index 8d19ca5698..70600f7f7c 100644 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt @@ -31,6 +31,7 @@ interface VerifySessionEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onEnterRecoveryKey() + fun onCreateNewRecoveryKey() fun onDone() } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt index cc97faa6e3..222683156a 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt @@ -34,17 +34,7 @@ class VerifySelfSessionNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: VerifySelfSessionPresenter, ) : Node(buildContext, plugins = plugins) { - private fun onEnterRecoveryKey() { - plugins().forEach { - it.onEnterRecoveryKey() - } - } - - private fun onDone() { - plugins().forEach { - it.onDone() - } - } + private val callback = plugins().first() @Composable override fun View(modifier: Modifier) { @@ -52,8 +42,9 @@ class VerifySelfSessionNode @AssistedInject constructor( VerifySelfSessionView( state = state, modifier = modifier, - onEnterRecoveryKey = ::onEnterRecoveryKey, - goBack = ::onDone, + onEnterRecoveryKey = callback::onEnterRecoveryKey, + onCreateNewRecoveryKey = callback::onCreateNewRecoveryKey, + onFinished = callback::onDone, ) } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt index 2a7740d8d0..c09b48946d 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt @@ -23,10 +23,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import com.freeletics.flowredux.compose.rememberStateAndDispatch 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.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -35,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState @@ -43,20 +48,28 @@ class VerifySelfSessionPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, private val encryptionService: EncryptionService, private val stateMachine: VerifySelfSessionStateMachine, + private val buildMeta: BuildMeta, ) : Presenter { @Composable override fun present(): VerifySelfSessionState { + val coroutineScope = rememberCoroutineScope() LaunchedEffect(Unit) { // Force reset, just in case the service was left in a broken state sessionVerificationService.reset() } val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val stateAndDispatch = stateMachine.rememberStateAndDispatch() + var skipVerification by remember { mutableStateOf(false) } + val needsVerification by sessionVerificationService.needsVerificationFlow.collectAsState() val verificationFlowStep by remember { derivedStateOf { - stateAndDispatch.state.value.toVerificationStep( - canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE - ) + when { + skipVerification -> VerifySelfSessionState.VerificationStep.Skipped + needsVerification -> stateAndDispatch.state.value.toVerificationStep( + canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE + ) + else -> VerifySelfSessionState.VerificationStep.Completed + } } } // Start this after observing state machine @@ -68,14 +81,19 @@ class VerifySelfSessionPresenter @Inject constructor( when (event) { VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification) VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification) - VerifySelfSessionViewEvents.Restart -> stateAndDispatch.dispatchAction(StateMachineEvent.Restart) VerifySelfSessionViewEvents.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge) VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) - VerifySelfSessionViewEvents.CancelAndClose -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) + VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) + VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset) + VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch { + sessionVerificationService.saveVerifiedState(true) + skipVerification = true + } } } return VerifySelfSessionState( verificationFlowStep = verificationFlowStep, + displaySkipButton = buildMeta.isDebuggable, eventSink = ::handleEvents, ) } @@ -85,7 +103,7 @@ class VerifySelfSessionPresenter @Inject constructor( ): VerifySelfSessionState.VerificationStep = when (val machineState = this) { StateMachineState.Initial, null -> { - VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = canEnterRecoveryKey) + VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = canEnterRecoveryKey, isLastDevice = encryptionService.isLastDevice.value) } StateMachineState.RequestingVerification, StateMachineState.StartingSasVerification, @@ -118,7 +136,7 @@ class VerifySelfSessionPresenter @Inject constructor( private fun CoroutineScope.observeVerificationService() { sessionVerificationService.verificationFlowState.onEach { verificationAttemptState -> when (verificationAttemptState) { - VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Restart) + VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Reset) VerificationFlowState.AcceptedVerificationRequest -> { stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt index fa3cb68adf..30d91b8fa4 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt @@ -24,15 +24,17 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD @Immutable data class VerifySelfSessionState( val verificationFlowStep: VerificationStep, + val displaySkipButton: Boolean, val eventSink: (VerifySelfSessionViewEvents) -> Unit, ) { @Stable sealed interface VerificationStep { - data class Initial(val canEnterRecoveryKey: Boolean) : VerificationStep + data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean) : VerificationStep data object Canceled : VerificationStep data object AwaitingOtherDeviceResponse : VerificationStep data object Ready : VerificationStep data class Verifying(val data: SessionVerificationData, val state: AsyncData) : VerificationStep data object Completed : VerificationStep + data object Skipped : VerificationStep } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt index 2a1c63370e..29a484521a 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt @@ -20,24 +20,35 @@ package io.element.android.features.verifysession.impl import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationService import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.timeout import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds import com.freeletics.flowredux.dsl.State as MachineState +@OptIn(FlowPreview::class) class VerifySelfSessionStateMachine @Inject constructor( private val sessionVerificationService: SessionVerificationService, + private val encryptionService: EncryptionService, ) : FlowReduxStateMachine( initialState = State.Initial ) { init { spec { inState { - on { _: Event.RequestVerification, state: MachineState -> + on { _: Event.RequestVerification, state -> state.override { State.RequestingVerification } } - on { _: Event.StartSasVerification, state: MachineState -> + on { _: Event.StartSasVerification, state -> state.override { State.StartingSasVerification } } } @@ -45,12 +56,9 @@ class VerifySelfSessionStateMachine @Inject constructor( onEnterEffect { sessionVerificationService.requestVerification() } - on { _: Event.DidAcceptVerificationRequest, state: MachineState -> + on { _: Event.DidAcceptVerificationRequest, state -> state.override { State.VerificationRequestAccepted } } - on { _: Event.DidFail, state: MachineState -> - state.override { State.Initial } - } } inState { onEnterEffect { @@ -58,25 +66,28 @@ class VerifySelfSessionStateMachine @Inject constructor( } } inState { - on { _: Event.StartSasVerification, state: MachineState -> + on { _: Event.StartSasVerification, state -> state.override { State.StartingSasVerification } } } inState { - on { _: Event.Restart, state: MachineState -> + on { _: Event.RequestVerification, state -> state.override { State.RequestingVerification } } + on { _: Event.Reset, state -> + state.override { State.Initial } + } } inState { - on { event: Event.DidReceiveChallenge, state: MachineState -> + on { event: Event.DidReceiveChallenge, state -> state.override { State.Verifying.ChallengeReceived(event.data) } } } inState { - on { _: Event.AcceptChallenge, state: MachineState -> + on { _: Event.AcceptChallenge, state -> state.override { State.Verifying.Replying(state.snapshot.data, accept = true) } } - on { _: Event.DeclineChallenge, state: MachineState -> + on { _: Event.DeclineChallenge, state -> state.override { State.Verifying.Replying(state.snapshot.data, accept = false) } } } @@ -88,11 +99,21 @@ class VerifySelfSessionStateMachine @Inject constructor( sessionVerificationService.declineVerification() } } - on { _: Event.DidAcceptChallenge, state: MachineState -> + on { _: Event.DidAcceptChallenge, state -> + // If a key backup exists, wait until it's restored or a timeout happens + val hasBackup = encryptionService.doesBackupExistOnServer().getOrNull().orFalse() + if (hasBackup) { + tryOrNull { + encryptionService.recoveryStateStateFlow.filter { it == RecoveryState.ENABLED } + .timeout(10.seconds) + .first() + } + } state.override { State.Completed } } } inState { + // TODO The 'Canceling' -> 'Canceled' transitions doesn't seem to work anymore, check if something changed in the Rust SDK onEnterEffect { sessionVerificationService.cancelVerification() } @@ -102,21 +123,24 @@ class VerifySelfSessionStateMachine @Inject constructor( state.override { State.SasVerificationStarted } } on { _: Event.Cancel, state: MachineState -> - if (state.snapshot in sequenceOf( - State.Initial, - State.Completed, - State.Canceled - )) { - state.noChange() - } else { - state.override { State.Canceling } + when (state.snapshot) { + State.Initial, State.Completed, State.Canceled -> state.noChange() + // For some reason `cancelVerification` is not calling its delegate `didCancel` method so we don't pass from + // `Canceling` state to `Canceled` automatically anymore + else -> { + sessionVerificationService.cancelVerification() + state.override { State.Canceled } + } } } on { _: Event.DidCancel, state: MachineState -> state.override { State.Canceled } } on { _: Event.DidFail, state: MachineState -> - state.override { State.Canceled } + when (state.snapshot) { + is State.RequestingVerification -> state.override { State.Initial } + else -> state.override { State.Canceled } + } } } } @@ -190,7 +214,7 @@ class VerifySelfSessionStateMachine @Inject constructor( /** Request failed. */ data object DidFail : Event - /** Restart the verification flow. */ - data object Restart : Event + /** Reset the verification flow to the initial state. */ + data object Reset : Event } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt index 59d42f11cd..c066d48613 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.verifysession.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.VerificationEmoji @@ -24,27 +25,34 @@ import io.element.android.libraries.matrix.api.verification.VerificationEmoji open class VerifySelfSessionStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVerifySelfSessionState(), + aVerifySelfSessionState(displaySkipButton = true), aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse + verificationFlowStep = VerificationStep.AwaitingOtherDeviceResponse ), aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized) + verificationFlowStep = VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized) ), aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) + verificationFlowStep = VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) ), aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled + verificationFlowStep = VerificationStep.Canceled ), aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready + verificationFlowStep = VerificationStep.Ready ), aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized) + verificationFlowStep = VerificationStep.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized) ), aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true) + verificationFlowStep = VerificationStep.Initial(canEnterRecoveryKey = true, isLastDevice = false) + ), + aVerifySelfSessionState( + verificationFlowStep = VerificationStep.Initial(canEnterRecoveryKey = true, isLastDevice = true) + ), + aVerifySelfSessionState( + verificationFlowStep = VerificationStep.Completed, + displaySkipButton = true, ), // Add other state here ) @@ -63,10 +71,12 @@ private fun aDecimalsSessionVerificationData( } internal fun aVerifySelfSessionState( - verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false), + verificationFlowStep: VerificationStep = VerificationStep.Initial(canEnterRecoveryKey = false, isLastDevice = false), + displaySkipButton: Boolean = false, eventSink: (VerifySelfSessionViewEvents) -> Unit = {}, ) = VerifySelfSessionState( verificationFlowStep = verificationFlowStep, + displaySkipButton = displaySkipButton, eventSink = eventSink, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt index 69fdfc5dfc..e54ba31872 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt @@ -28,11 +28,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -42,56 +43,81 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.verifysession.impl.emoji.toEmojiResource import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.PageTitle 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.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep +@OptIn(ExperimentalMaterial3Api::class) @Composable fun VerifySelfSessionView( state: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit, - goBack: () -> Unit, + onCreateNewRecoveryKey: () -> Unit, + onFinished: () -> Unit, modifier: Modifier = Modifier, ) { - fun goBackAndCancelIfNeeded() { - state.eventSink(VerifySelfSessionViewEvents.CancelAndClose) - goBack() + fun resetFlow() { + state.eventSink(VerifySelfSessionViewEvents.Reset) } - if (state.verificationFlowStep is FlowStep.Completed) { - goBack() + val updatedOnFinished by rememberUpdatedState(newValue = onFinished) + LaunchedEffect(state.verificationFlowStep, updatedOnFinished) { + if (state.verificationFlowStep is FlowStep.Skipped) { + updatedOnFinished() + } } BackHandler { - goBackAndCancelIfNeeded() + when (state.verificationFlowStep) { + is FlowStep.Canceled -> resetFlow() + is FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel) + is FlowStep.Verifying -> { + if (!state.verificationFlowStep.state.isLoading()) { + state.eventSink(VerifySelfSessionViewEvents.DeclineVerification) + } + } + else -> Unit + } } val verificationFlowStep = state.verificationFlowStep - val buttonsVisible by remember(verificationFlowStep) { - derivedStateOf { verificationFlowStep != FlowStep.AwaitingOtherDeviceResponse && verificationFlowStep != FlowStep.Completed } - } HeaderFooterPage( modifier = modifier, + topBar = { + TopAppBar( + title = {}, + actions = { + if (state.displaySkipButton && state.verificationFlowStep != FlowStep.Completed) { + TextButton( + text = stringResource(CommonStrings.action_skip), + onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) } + ) + } + } + ) + }, header = { HeaderContent(verificationFlowStep = verificationFlowStep) }, footer = { - if (buttonsVisible) { - BottomMenu( - screenState = state, - goBack = ::goBackAndCancelIfNeeded, - onEnterRecoveryKey = onEnterRecoveryKey - ) - } + BottomMenu( + screenState = state, + goBack = ::resetFlow, + onEnterRecoveryKey = onEnterRecoveryKey, + onCreateNewRecoveryKey = onCreateNewRecoveryKey, + onFinished = onFinished, + ) } ) { Content(flowState = verificationFlowStep) @@ -100,61 +126,52 @@ fun VerifySelfSessionView( @Composable private fun HeaderContent(verificationFlowStep: FlowStep) { - val iconResourceId = when (verificationFlowStep) { - is FlowStep.Initial -> R.drawable.ic_verification_devices - FlowStep.Canceled -> R.drawable.ic_verification_warning - FlowStep.AwaitingOtherDeviceResponse -> R.drawable.ic_verification_waiting - FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.drawable.ic_verification_emoji + val iconStyle = when (verificationFlowStep) { + is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.LockSolid()) + FlowStep.Canceled -> BigIcon.Style.AlertSolid + FlowStep.Ready, is FlowStep.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction()) + FlowStep.Completed -> BigIcon.Style.SuccessSolid + is FlowStep.Skipped -> return } val titleTextId = when (verificationFlowStep) { - is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title + is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title FlowStep.Canceled -> CommonStrings.common_verification_cancelled - FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_title - FlowStep.Ready, - FlowStep.Completed -> R.string.screen_session_verification_compare_emojis_title + FlowStep.Ready -> R.string.screen_session_verification_compare_emojis_title + FlowStep.Completed -> R.string.screen_identity_confirmed_title is FlowStep.Verifying -> when (verificationFlowStep.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title } + is FlowStep.Skipped -> return } val subtitleTextId = when (verificationFlowStep) { - is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle + is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle - FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_subtitle FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle - FlowStep.Completed -> R.string.screen_session_verification_compare_emojis_subtitle + FlowStep.Completed -> R.string.screen_identity_confirmed_subtitle is FlowStep.Verifying -> when (verificationFlowStep.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle } + is FlowStep.Skipped -> return } - IconTitleSubtitleMolecule( - modifier = Modifier.padding(top = 60.dp), - iconResourceId = iconResourceId, + PageTitle( + iconStyle = iconStyle, title = stringResource(id = titleTextId), - subTitle = stringResource(id = subtitleTextId) + subtitle = stringResource(id = subtitleTextId) ) } @Composable private fun Content(flowState: FlowStep) { Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { - when (flowState) { - is FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit - FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting() - is FlowStep.Verifying -> ContentVerifying(flowState) + if (flowState is FlowStep.Verifying) { + ContentVerifying(flowState) } } } -@Composable -private fun ContentWaiting() { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - CircularProgressIndicator() - } -} - @Composable private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) { when (verificationFlowStep.data) { @@ -211,77 +228,114 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie private fun BottomMenu( screenState: VerifySelfSessionState, onEnterRecoveryKey: () -> Unit, + onCreateNewRecoveryKey: () -> Unit, goBack: () -> Unit, + onFinished: () -> Unit, ) { val verificationViewState = screenState.verificationFlowStep val eventSink = screenState.eventSink val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading - val positiveButtonTitle = when (verificationViewState) { - is FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial - FlowStep.Canceled -> R.string.screen_session_verification_positive_button_canceled - is FlowStep.Verifying -> { - if (isVerifying) { - R.string.screen_session_verification_positive_button_verifying_ongoing + + when (verificationViewState) { + is FlowStep.Initial -> { + if (verificationViewState.isLastDevice) { + BottomMenu( + positiveButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key), + onPositiveButtonClicked = onEnterRecoveryKey, + negativeButtonTitle = stringResource(R.string.screen_identity_confirmation_create_new_recovery_key), + onNegativeButtonClicked = onCreateNewRecoveryKey, + ) } else { - R.string.screen_session_verification_they_match + BottomMenu( + positiveButtonTitle = stringResource(R.string.screen_identity_use_another_device), + onPositiveButtonClicked = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, + negativeButtonTitle = stringResource(R.string.screen_session_verification_enter_recovery_key), + onNegativeButtonClicked = onEnterRecoveryKey, + ) } } - FlowStep.Ready -> CommonStrings.action_start - else -> null - } - val negativeButtonTitle = when (verificationViewState) { - is FlowStep.Initial -> CommonStrings.action_cancel - FlowStep.Canceled -> CommonStrings.action_cancel - is FlowStep.Verifying -> R.string.screen_session_verification_they_dont_match - else -> null - } - val negativeButtonEnabled = !isVerifying - - val positiveButtonEvent = when (verificationViewState) { - is FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification - FlowStep.Ready -> VerifySelfSessionViewEvents.StartSasVerification - is FlowStep.Verifying -> if (!isVerifying) VerifySelfSessionViewEvents.ConfirmVerification else null - FlowStep.Canceled -> VerifySelfSessionViewEvents.Restart - else -> null - } - - val negativeButtonCallback: () -> Unit = when (verificationViewState) { - is FlowStep.Verifying -> { - { eventSink(VerifySelfSessionViewEvents.DeclineVerification) } + is FlowStep.Canceled -> { + BottomMenu( + positiveButtonTitle = stringResource(R.string.screen_session_verification_positive_button_canceled), + onPositiveButtonClicked = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, + negativeButtonTitle = stringResource(CommonStrings.action_cancel), + onNegativeButtonClicked = goBack, + ) } - else -> goBack + is FlowStep.Ready -> { + BottomMenu( + positiveButtonTitle = stringResource(CommonStrings.action_start), + onPositiveButtonClicked = { eventSink(VerifySelfSessionViewEvents.StartSasVerification) }, + negativeButtonTitle = stringResource(CommonStrings.action_cancel), + onNegativeButtonClicked = goBack, + ) + } + is FlowStep.AwaitingOtherDeviceResponse -> { + BottomMenu( + positiveButtonTitle = stringResource(R.string.screen_identity_waiting_on_other_device), + onPositiveButtonClicked = {}, + isLoading = true, + ) + } + is FlowStep.Verifying -> { + val positiveButtonTitle = if (isVerifying) { + stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing) + } else { + stringResource(R.string.screen_session_verification_they_match) + } + BottomMenu( + positiveButtonTitle = positiveButtonTitle, + onPositiveButtonClicked = { + if (!isVerifying) { + eventSink(VerifySelfSessionViewEvents.ConfirmVerification) + } + }, + negativeButtonTitle = stringResource(R.string.screen_session_verification_they_dont_match), + onNegativeButtonClicked = { eventSink(VerifySelfSessionViewEvents.DeclineVerification) }, + isLoading = isVerifying, + ) + } + is FlowStep.Completed -> { + BottomMenu( + positiveButtonTitle = stringResource(CommonStrings.action_continue), + onPositiveButtonClicked = onFinished, + ) + } + is FlowStep.Skipped -> return } +} +@Composable +private fun BottomMenu( + positiveButtonTitle: String?, + onPositiveButtonClicked: () -> Unit, + modifier: Modifier = Modifier, + negativeButtonTitle: String? = null, + negativeButtonEnabled: Boolean = negativeButtonTitle != null, + onNegativeButtonClicked: () -> Unit = {}, + isLoading: Boolean = false, +) { ButtonColumnMolecule( - modifier = Modifier.padding(bottom = 20.dp) + modifier = modifier.padding(bottom = 16.dp) ) { if (positiveButtonTitle != null) { Button( - text = stringResource(positiveButtonTitle), - showProgress = isVerifying, + text = positiveButtonTitle, + showProgress = isLoading, modifier = Modifier.fillMaxWidth(), - onClick = { positiveButtonEvent?.let { eventSink(it) } } + onClick = onPositiveButtonClicked, ) } if (negativeButtonTitle != null) { TextButton( - text = stringResource(negativeButtonTitle), + text = negativeButtonTitle, modifier = Modifier.fillMaxWidth(), - onClick = negativeButtonCallback, + onClick = onNegativeButtonClicked, enabled = negativeButtonEnabled, ) - } - if (verificationViewState is FlowStep.Initial && verificationViewState.canEnterRecoveryKey) { - Text( - text = stringResource(id = CommonStrings.common_or), - color = ElementTheme.colors.textSecondary, - ) - TextButton( - text = stringResource(R.string.screen_session_verification_enter_recovery_key), - modifier = Modifier.fillMaxWidth(), - onClick = onEnterRecoveryKey, - ) + } else { + Spacer(modifier = Modifier.height(48.dp)) } } } @@ -292,6 +346,7 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta VerifySelfSessionView( state = state, onEnterRecoveryKey = {}, - goBack = {}, + onCreateNewRecoveryKey = {}, + onFinished = {}, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt index 8f9c69085c..2c6b776f7b 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt @@ -19,8 +19,9 @@ package io.element.android.features.verifysession.impl sealed interface VerifySelfSessionViewEvents { data object RequestVerification : VerifySelfSessionViewEvents data object StartSasVerification : VerifySelfSessionViewEvents - data object Restart : VerifySelfSessionViewEvents data object ConfirmVerification : VerifySelfSessionViewEvents data object DeclineVerification : VerifySelfSessionViewEvents - data object CancelAndClose : VerifySelfSessionViewEvents + data object Cancel : VerifySelfSessionViewEvents + data object Reset : VerifySelfSessionViewEvents + data object SkipVerification : VerifySelfSessionViewEvents } diff --git a/features/verifysession/impl/src/main/res/drawable/ic_verification_devices.xml b/features/verifysession/impl/src/main/res/drawable/ic_verification_devices.xml deleted file mode 100644 index 8ae6dd30fa..0000000000 --- a/features/verifysession/impl/src/main/res/drawable/ic_verification_devices.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/features/verifysession/impl/src/main/res/drawable/ic_verification_emoji.xml b/features/verifysession/impl/src/main/res/drawable/ic_verification_emoji.xml deleted file mode 100644 index 82583a4011..0000000000 --- a/features/verifysession/impl/src/main/res/drawable/ic_verification_emoji.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/features/verifysession/impl/src/main/res/drawable/ic_verification_waiting.xml b/features/verifysession/impl/src/main/res/drawable/ic_verification_waiting.xml deleted file mode 100644 index 5b9f2e3cfc..0000000000 --- a/features/verifysession/impl/src/main/res/drawable/ic_verification_waiting.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/features/verifysession/impl/src/main/res/drawable/ic_verification_warning.xml b/features/verifysession/impl/src/main/res/drawable/ic_verification_warning.xml deleted file mode 100644 index 882ac62cd7..0000000000 --- a/features/verifysession/impl/src/main/res/drawable/ic_verification_warning.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/features/verifysession/impl/src/main/res/values-be/translations.xml b/features/verifysession/impl/src/main/res/values-be/translations.xml index 57786fec8d..8b2c5e920d 100644 --- a/features/verifysession/impl/src/main/res/values-be/translations.xml +++ b/features/verifysession/impl/src/main/res/values-be/translations.xml @@ -1,5 +1,12 @@ + "Стварыць новы ключ аднаўлення" + "Пацвердзіце гэтую прыладу, каб наладзіць бяспечны абмен паведамленнямі." + "Пацвердзіце, што гэта вы" + "Цяпер вы можаце бяспечна чытаць і адпраўляць паведамленні, і ўсе, з кім вы маеце зносіны ў чаце, таксама могуць давяраць гэтай прыладзе." + "Прылада праверана" + "Выкарыстоўвайце іншую прыладу" + "Чаканне на іншай прыладзе…" "Здаецца, нешта не так. Альбо час чакання запыту скончыўся, альбо запыт быў адхілены." "Пераканайцеся, што прыведзеныя ніжэй эмодзі супадаюць з эмодзі, паказанымі ў вашым іншым сеансе." "Параўнайце эмодзі" diff --git a/features/verifysession/impl/src/main/res/values-cs/translations.xml b/features/verifysession/impl/src/main/res/values-cs/translations.xml index 3dd49ce9f2..fadcba869c 100644 --- a/features/verifysession/impl/src/main/res/values-cs/translations.xml +++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml @@ -1,5 +1,12 @@ + "Vytvoření nového klíče pro obnovení" + "Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv." + "Potvrďte, že jste to vy" + "Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat." + "Zařízení ověřeno" + "Použít jiné zařízení" + "Čekání na jiném zařízení…" "Něco není v pořádku. Buď vypršel časový limit požadavku, nebo byl požadavek zamítnut." "Zkontrolujte, zda se níže uvedené emotikony shodují s emotikony zobrazenými na jiné relaci." "Porovnání emotikonů" diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml index dd308b215d..baad607403 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,12 @@ + "Erstelle einen neuen Wiederherstellungsschlüssel" + "Verifiziere dieses Gerät, um sicheres Messaging einzurichten." + "Bestätige, dass du es bist" + "Du kannst nun verschlüsselte Nachrichten lesen oder versenden." + "Gerät verifiziert" + "Ein anderes Gerät verwenden" + "Bitte warten bis das andere Gerät bereit ist." "Etwas scheint nicht zu stimmen. Entweder ist das Zeitlimit für die Anfrage abgelaufen oder die Anfrage wurde abgelehnt." "Vergewissere dich dass die folgenden Emojis mit denen in deiner anderen Session übereinstimmen." "Emojis vergleichen" diff --git a/features/verifysession/impl/src/main/res/values-fr/translations.xml b/features/verifysession/impl/src/main/res/values-fr/translations.xml index 7b57eba9a6..a37a51f8c5 100644 --- a/features/verifysession/impl/src/main/res/values-fr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,11 @@ + "Vérifier cette session pour configurer votre messagerie sécurisée." + "Confirmez votre identité" + "Vous pouvez désormais lire ou envoyer des messages en toute sécurité, et toute personne avec qui vous discutez peut également faire confiance à cette session." + "Session vérifiée" + "Utiliser une autre session" + "En attente d’une autre session…" "Quelque chose ne va pas. Soit la demande a expiré, soit elle a été refusée." "Confirmez que les emojis ci-dessous correspondent à ceux affichés sur votre autre session." "Comparez les émojis" diff --git a/features/verifysession/impl/src/main/res/values-hu/translations.xml b/features/verifysession/impl/src/main/res/values-hu/translations.xml index aac80254ac..c31adb9736 100644 --- a/features/verifysession/impl/src/main/res/values-hu/translations.xml +++ b/features/verifysession/impl/src/main/res/values-hu/translations.xml @@ -1,5 +1,12 @@ + "Új helyreállítási kulcs létrehozása" + "A biztonságos üzenetkezelés beállításához ellenőrizze ezt az eszközt." + "Erősítse meg, hogy Ön az" + "Mostantól biztonságosan olvashat vagy küldhet üzeneteket, és bármelyik csevegőpartnere megbízhat ebben az eszközben." + "Eszköz ellenőrizve" + "Másik eszköz használata" + "Várakozás a másik eszközre…" "Valami hibásnak tűnik. A kérés vagy időtúllépésre futott, vagy elutasították." "Erősítse meg, hogy a lenti emodzsik egyeznek-e a másik munkamenetben megjelenítettekkel." "Emodzsik összehasonlítása" diff --git a/features/verifysession/impl/src/main/res/values-in/translations.xml b/features/verifysession/impl/src/main/res/values-in/translations.xml index f498f7a52b..3a3bec959e 100644 --- a/features/verifysession/impl/src/main/res/values-in/translations.xml +++ b/features/verifysession/impl/src/main/res/values-in/translations.xml @@ -1,5 +1,12 @@ + "Buat kunci pemulihan baru" + "Verifikasi perangkat ini untuk menyiapkan perpesanan aman." + "Konfirmasi bahwa ini Anda" + "Sekarang Anda dapat membaca atau mengirim pesan dengan aman, dan siapa pun yang mengobrol dengan Anda juga dapat mempercayai perangkat ini." + "Perangkat terverifikasi" + "Gunakan perangkat lain" + "Menunggu di perangkat lain…" "Sepertinya ada yang tidak beres. Entah permintaan sudah habis masa berlakunya atau permintaan ditolak." "Konfirmasikan bahwa emoji di bawah ini sesuai dengan yang ditampilkan pada sesi Anda yang lain." "Bandingkan emoji" diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml index 6fb227308e..a3b300793d 100644 --- a/features/verifysession/impl/src/main/res/values-it/translations.xml +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -1,5 +1,11 @@ + "Verifica questo dispositivo per segnare i tuoi messaggi come sicuri." + "Conferma la tua identità" + "Ora puoi leggere o inviare messaggi in tutta sicurezza e anche chi chatta con te può fidarsi di questo dispositivo." + "Dispositivo verificato" + "Usa un altro dispositivo" + "In attesa sull\'altro dispositivo…" "C\'è qualcosa che non va. La richiesta è scaduta o è stata rifiutata." "Verifica che gli emoji sottostanti corrispondano a quelli mostrati nell\'altra sessione." "Confronta le emoji" diff --git a/features/verifysession/impl/src/main/res/values-ru/translations.xml b/features/verifysession/impl/src/main/res/values-ru/translations.xml index 69311e39e7..c89077d172 100644 --- a/features/verifysession/impl/src/main/res/values-ru/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml @@ -1,12 +1,25 @@ + + "Создайте новый " + "ключ восстановления" + + "Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями." + "Подтвердите, что это вы" + "Теперь вы можете безопасно читать и отправлять сообщения, и все, с кем вы общаетесь в чате, также могут доверять этому устройству." + "Устройство проверено" + "Используйте другое устройство" + "Ожидание на другом устройстве…" "Похоже, что-то не так. Время ожидания запроса либо истекло, либо запрос был отклонен." "Убедитесь, что приведенные ниже емоджи совпадают с емоджи показанными во время другого сеанса." "Сравните емодзи" "Убедитесь, что приведенные ниже числа совпадают с цифрами, показанными в другом сеансе." "Сравните числа" "Ваш новый сеанс подтвержден. У него есть доступ к вашим зашифрованным сообщениям, и другие пользователи увидят его как доверенное." - "Введите ключ восстановления" + + "Введите " + "ключ восстановления" + "Чтобы получить доступ к зашифрованной истории сообщений, докажите, что это вы." "Открыть существующий сеанс" "Повторить проверку" diff --git a/features/verifysession/impl/src/main/res/values-sk/translations.xml b/features/verifysession/impl/src/main/res/values-sk/translations.xml index 6452b85fcb..b3089b7b30 100644 --- a/features/verifysession/impl/src/main/res/values-sk/translations.xml +++ b/features/verifysession/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,12 @@ + "Vytvoriť nový kľúč na obnovenie" + "Ak chcete nastaviť zabezpečené správy, overte toto zariadenie." + "Potvrďte, že ste to vy" + "Teraz môžete bezpečne čítať alebo odosielať správy a tomuto zariadeniu môže dôverovať aj ktokoľvek, s kým konverzujete." + "Zariadenie overené" + "Použiť iné zariadenie" + "Čaká sa na druhom zariadení…" "Zdá sa, že niečo nie je v poriadku. Časový limit žiadosti vypršal alebo bola žiadosť zamietnutá." "Skontrolujte, či sa emotikony uvedené nižšie zhodujú s emotikonmi zobrazenými vo vašej druhej relácii." "Porovnajte emotikony" diff --git a/features/verifysession/impl/src/main/res/values-uk/translations.xml b/features/verifysession/impl/src/main/res/values-uk/translations.xml index beed211c15..9be10a2bb0 100644 --- a/features/verifysession/impl/src/main/res/values-uk/translations.xml +++ b/features/verifysession/impl/src/main/res/values-uk/translations.xml @@ -1,5 +1,9 @@ + "Перевірте цей пристрій, щоб налаштувати безпечний обмін повідомленнями." + "Підтвердіть, що це ви" + "Тепер ви можете безпечно читати або надсилати повідомлення, і кожен, з ким ви спілкуєтесь, також може довіряти цьому пристрою." + "Пристрій перевірено" "Щось не так. Або час очікування запиту минув, або в запиті було відмовлено." "Переконайтеся, що емодзі нижче збігаються з тими, що відображаються під час іншого сеансу." "Порівняти емодзі" diff --git a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml index ed85823769..d73ea8e2c4 100644 --- a/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,5 +1,8 @@ + "裝置已認證" + "使用另一個裝置" + "正在等待其他裝置……" "似乎出了一點問題。有可能是因為等候逾時,或是請求被拒絕。" "確認顯示在其他工作階段上的表情符號是否和下方的相同。" "比對表情符號" diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index b46954f42b..41cd2f8dbc 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -1,5 +1,12 @@ + "Create a new recovery key" + "Verify this device to set up secure messaging." + "Confirm that it\'s you" + "Now you can read or send messages securely, and anyone you chat with can also trust this device." + "Device verified" + "Use another device" + "Waiting on other device…" "Something doesn’t seem right. Either the request timed out or the request was denied." "Confirm that the emojis below match those shown on your other session." "Compare emojis" diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index ad128b450d..06f69e1628 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -23,15 +23,19 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -48,7 +52,21 @@ class VerifySelfSessionPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + awaitItem().run { + assertThat(verificationFlowStep).isEqualTo(VerificationStep.Initial(false, false)) + assertThat(displaySkipButton).isTrue() + } + } + } + + @Test + fun `present - hides skip verification button on non-debuggable builds`() = runTest { + val buildMeta = aBuildMeta(isDebuggable = false) + val presenter = createVerifySelfSessionPresenter(buildMeta = buildMeta) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().displaySkipButton).isFalse() } } @@ -62,13 +80,28 @@ class VerifySelfSessionPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true)) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true, false)) + } + } + + @Test + fun `present - Initial state is received, can use recovery key and is last device`() = runTest { + val presenter = createVerifySelfSessionPresenter( + encryptionService = FakeEncryptionService().apply { + emitIsLastDevice(true) + emitRecoveryState(RecoveryState.INCOMPLETE) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true, true)) } } @Test fun `present - Handles requestVerification`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -79,13 +112,13 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Handles startSasVerification`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false, false)) val eventSink = initialState.eventSink eventSink(VerifySelfSessionViewEvents.StartSasVerification) // Await for other device response: @@ -104,16 +137,16 @@ class VerifySelfSessionPresenterTests { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false, false)) val eventSink = initialState.eventSink - eventSink(VerifySelfSessionViewEvents.CancelAndClose) + eventSink(VerifySelfSessionViewEvents.Cancel) expectNoEvents() } } @Test - fun `present - A fail in the flow cancels it`() = runTest { - val service = FakeSessionVerificationService() + fun `present - A failure when verifying cancels it`() = runTest { + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -128,23 +161,37 @@ class VerifySelfSessionPresenterTests { } } + @Test + fun `present - A fail when requesting verification resets the state to the initial one`() = runTest { + val service = unverifiedSessionService() + val presenter = createVerifySelfSessionPresenter(service) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + service.shouldFail = true + awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification) + service.shouldFail = false + assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.AwaitingOtherDeviceResponse::class.java) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false, false)) + } + } + @Test fun `present - Canceling the flow once it's verifying cancels it`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(VerifySelfSessionViewEvents.CancelAndClose) - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) + state.eventSink(VerifySelfSessionViewEvents.Cancel) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) } } @Test fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -157,7 +204,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Restart after cancelation returns to requesting verification`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -165,19 +212,36 @@ class VerifySelfSessionPresenterTests { val state = requestVerificationAndAwaitVerifyingState(service) service.givenVerificationFlowState(VerificationFlowState.Canceled) assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) - state.eventSink(VerifySelfSessionViewEvents.Restart) + state.eventSink(VerifySelfSessionViewEvents.RequestVerification) // Went back to requesting verification assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse) cancelAndIgnoreRemainingEvents() } } + @Test + fun `present - Go back after cancelation returns to initial state`() = runTest { + val service = unverifiedSessionService() + val presenter = createVerifySelfSessionPresenter(service) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = requestVerificationAndAwaitVerifyingState(service) + service.givenVerificationFlowState(VerificationFlowState.Canceled) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled) + state.eventSink(VerifySelfSessionViewEvents.Reset) + // Went back to initial state + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false, false)) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - When verification is approved, the flow completes if there is no error`() = runTest { val emojis = listOf( VerificationEmoji(number = 30, emoji = "😀", description = "Smiley") ) - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -199,7 +263,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - When verification is declined, the flow is canceled`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -216,12 +280,39 @@ class VerifySelfSessionPresenterTests { } } + @Test + fun `present - Skip event skips the flow`() = runTest { + val service = unverifiedSessionService() + val presenter = createVerifySelfSessionPresenter(service) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(VerifySelfSessionViewEvents.SkipVerification) + service.saveVerifiedStateResult.assertions().isCalledOnce().with(value(true)) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped) + } + } + + @Test + fun `present - When verification is not needed, the flow is completed`() = runTest { + val service = FakeSessionVerificationService().apply { + givenNeedsVerification(false) + } + val presenter = createVerifySelfSessionPresenter(service) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed) + } + } + private suspend fun ReceiveTurbine.requestVerificationAndAwaitVerifyingState( fakeService: FakeSessionVerificationService, sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), ): VerifySelfSessionState { var state = awaitItem() - assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial(false, false)) state.eventSink(VerifySelfSessionViewEvents.RequestVerification) // Await for other device response: state = awaitItem() @@ -240,14 +331,23 @@ class VerifySelfSessionPresenterTests { return state } + private fun unverifiedSessionService(): FakeSessionVerificationService { + return FakeSessionVerificationService().apply { + givenVerifiedStatus(SessionVerifiedStatus.NotVerified) + givenNeedsVerification(true) + } + } + private fun createVerifySelfSessionPresenter( - service: SessionVerificationService = FakeSessionVerificationService(), + service: SessionVerificationService = unverifiedSessionService(), encryptionService: EncryptionService = FakeEncryptionService(), + buildMeta: BuildMeta = aBuildMeta(), ): VerifySelfSessionPresenter { return VerifySelfSessionPresenter( sessionVerificationService = service, encryptionService = encryptionService, - stateMachine = VerifySelfSessionStateMachine(service), + stateMachine = VerifySelfSessionStateMachine(service, encryptionService), + buildMeta = buildMeta, ) } } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt index 4dfad8c9c9..4d5f67f0b1 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt @@ -17,17 +17,20 @@ package io.element.android.features.verifysession.impl import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce import io.element.android.tests.testutils.pressBackKey import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -36,57 +39,101 @@ class VerifySelfSessionViewTest { @get:Rule val rule = createAndroidComposeRule() @Test - fun `clicking on cancel calls the expected callback and emits the expected Event`() { + fun `back key pressed - when canceled resets the flow`() { val eventsRecorder = EventsRecorder() - ensureCalledOnce { callback -> - rule.setContent { - VerifySelfSessionView( - aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), - eventSink = eventsRecorder - ), - onEnterRecoveryKey = EnsureNeverCalled(), - goBack = callback, - ) - } - rule.clickOn(CommonStrings.action_cancel) - } - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose) + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled, + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Reset) } @Test - fun `clicking on back key calls the expected callback and emits the expected Event`() { + fun `back key pressed - when awaiting response cancels the verification`() { val eventsRecorder = EventsRecorder() - ensureCalledOnce { callback -> - rule.setContent { - VerifySelfSessionView( - aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), - eventSink = eventsRecorder - ), - onEnterRecoveryKey = EnsureNeverCalled(), - goBack = callback, - ) - } - rule.pressBackKey() - } - eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose) + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse, + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel) } @Test - fun `when flow is completed, the expected callback is invoked`() { + fun `back key pressed - when ready to verify cancels the verification`() { + val eventsRecorder = EventsRecorder() + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready, + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.Cancel) + } + + @Test + fun `back key pressed - when verifying and not loading declines the verification`() { + val eventsRecorder = EventsRecorder() + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, + ), + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) + } + + @Test + fun `back key pressed - when verifying and loading does nothing`() { + val eventsRecorder = EventsRecorder() + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Loading(), + ), + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertEmpty() + } + + @Test + fun `back key pressed - on Completed step does nothing`() { + val eventsRecorder = EventsRecorder() + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed, + eventSink = eventsRecorder + ), + ) + rule.pressBackKey() + eventsRecorder.assertEmpty() + } + + @Test + fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setContent { - VerifySelfSessionView( - aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed, - eventSink = eventsRecorder - ), - onEnterRecoveryKey = EnsureNeverCalled(), - goBack = callback, - ) - } + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed, + eventSink = eventsRecorder + ), + onFinished = callback, + ) + rule.clickOn(CommonStrings.action_continue) } } @@ -95,36 +142,45 @@ class VerifySelfSessionViewTest { fun `clicking on enter recovery key calls the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> - rule.setContent { - VerifySelfSessionView( - aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), - eventSink = eventsRecorder - ), - onEnterRecoveryKey = callback, - goBack = EnsureNeverCalled(), - ) - } + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true, false), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = callback, + ) rule.clickOn(R.string.screen_session_verification_enter_recovery_key) } } + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on create new recovery key calls the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true, true), + eventSink = eventsRecorder + ), + onCreateNewRecoveryKey = callback, + ) + rule.clickOn(R.string.screen_identity_confirmation_create_new_recovery_key) + } + } + @Test fun `clicking on they match emits the expected event`() { val eventsRecorder = EventsRecorder() - rule.setContent { - VerifySelfSessionView( - aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( - data = aEmojisSessionVerificationData(), - state = AsyncData.Uninitialized, - ), - eventSink = eventsRecorder + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, ), - onEnterRecoveryKey = EnsureNeverCalled(), - goBack = EnsureNeverCalled(), - ) - } + eventSink = eventsRecorder + ), + ) rule.clickOn(R.string.screen_session_verification_they_match) eventsRecorder.assertSingle(VerifySelfSessionViewEvents.ConfirmVerification) } @@ -132,20 +188,60 @@ class VerifySelfSessionViewTest { @Test fun `clicking on they do not match emits the expected event`() { val eventsRecorder = EventsRecorder() - rule.setContent { - VerifySelfSessionView( - aVerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( - data = aEmojisSessionVerificationData(), - state = AsyncData.Uninitialized, - ), - eventSink = eventsRecorder + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, ), - onEnterRecoveryKey = EnsureNeverCalled(), - goBack = EnsureNeverCalled(), - ) - } + eventSink = eventsRecorder + ), + ) rule.clickOn(R.string.screen_session_verification_they_dont_match) eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) } + + @Test + fun `clicking on 'Skip' emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = true, isLastDevice = false), + displaySkipButton = true, + eventSink = eventsRecorder + ), + ) + rule.clickOn(CommonStrings.action_skip) + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.SkipVerification) + } + + @Test + fun `on Skipped step - onFinished callback is called immediately`() { + ensureCalledOnce { callback -> + rule.setVerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Skipped, + displaySkipButton = true, + eventSink = EnsureNeverCalledWithParam(), + ), + onFinished = callback, + ) + } + } + + private fun AndroidComposeTestRule.setVerifySelfSessionView( + state: VerifySelfSessionState, + onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), + onCreateNewRecoveryKey: () -> Unit = EnsureNeverCalled(), + onFinished: () -> Unit = EnsureNeverCalled(), + ) { + rule.setContent { + VerifySelfSessionView( + state = state, + onEnterRecoveryKey = onEnterRecoveryKey, + onCreateNewRecoveryKey = onCreateNewRecoveryKey, + onFinished = onFinished, + ) + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ccf78465d3..ac8dd451dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ [versions] # Project -android_gradle_plugin = "8.3.1" +android_gradle_plugin = "8.3.2" kotlin = "1.9.23" ksp = "1.9.23-1.0.19" firebaseAppDistribution = "4.2.0" @@ -18,7 +18,7 @@ activity = "1.8.2" media3 = "1.3.0" # Compose -compose_bom = "2024.03.00" +compose_bom = "2024.04.00" composecompiler = "1.5.11" # Coroutines @@ -33,16 +33,16 @@ test_core = "1.5.0" #other coil = "2.6.0" datetime = "0.5.0" -dependencyAnalysis = "1.30.0" +dependencyAnalysis = "1.31.0" serialization_json = "1.6.3" showkase = "1.0.2" appyx = "1.4.0" -sqldelight = "2.0.1" -wysiwyg = "2.34.0" +sqldelight = "2.0.2" +wysiwyg = "2.36.0" telephoto = "0.9.0" # DI -dagger = "2.51" +dagger = "2.51.1" anvil = "2.4.9" # Auto service @@ -120,7 +120,7 @@ network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" } network_okhttp_okhttp = { module = "com.squareup.okhttp3:okhttp" } network_okhttp = { module = "com.squareup.okhttp3:okhttp" } network_mockwebserver = { module = "com.squareup.okhttp3:mockwebserver" } -network_retrofit_bom = "com.squareup.retrofit2:retrofit-bom:2.10.0" +network_retrofit_bom = "com.squareup.retrofit2:retrofit-bom:2.11.0" network_retrofit = { module = "com.squareup.retrofit2:retrofit" } network_retrofit_converter_serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization" } @@ -136,7 +136,7 @@ test_konsist = "com.lemonappdev:konsist:0.13.0" test_turbine = "app.cash.turbine:turbine:1.1.0" test_truth = "com.google.truth:truth:1.4.2" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.15" -test_robolectric = "org.robolectric:robolectric:4.11.1" +test_robolectric = "org.robolectric:robolectric:4.12.1" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } # Others @@ -144,7 +144,7 @@ coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" } -compound = { module = "io.element.android:compound-android", version = "0.0.5" } +compound = { module = "io.element.android:compound-android", version = "0.0.6" } datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" } kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7" @@ -152,9 +152,9 @@ showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" } showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" } jsoup = "org.jsoup:jsoup:1.17.2" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } -molecule-runtime = "app.cash.molecule:molecule-runtime:1.4.1" +molecule-runtime = "app.cash.molecule:molecule-runtime:1.4.2" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.12" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.13" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } @@ -175,9 +175,11 @@ opusencoder = "io.element.android:opusencoder:1.1.0" kotlinpoet = "com.squareup:kotlinpoet:1.16.0" # Analytics -posthog = "com.posthog:posthog-android:3.1.15" -sentry = "io.sentry:sentry-android:7.6.0" -matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.14.0" +posthog = "com.posthog:posthog-android:3.1.16" +sentry = "io.sentry:sentry-android:7.8.0" +# Note: only 0.19.0 will compile properly +# main branch can be tested replacing the version with main-SNAPSHOT +matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.15.0" # Emojibase matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3" @@ -217,7 +219,7 @@ anvil = { id = "com.squareup.anvil", version.ref = "anvil" } detekt = "io.gitlab.arturbosch.detekt:1.23.6" ktlint = "org.jlleitschuh.gradle.ktlint:12.1.0" dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12" -dependencycheck = "org.owasp.dependencycheck:9.0.10" +dependencycheck = "org.owasp.dependencycheck:9.1.0" dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" } paparazzi = "app.cash.paparazzi:1.3.3" kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd49177..e6441136f3 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 865f1ba80d..fcbbad6dd6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=85719317abd2112f021d4f41f09ec370534ba288432065f4b477b6a3b652910d -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip +distributionSha256Sum=194717442575a6f96e1c1befa2c30e9a4fc90f701d7aee33eb879b79e7ff05c0 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85bee..7101f8e467 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 422307ffd9..737eab7ac7 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.androidutils.system +import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent @@ -73,6 +74,9 @@ fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResu val intent = Intent() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + if (this !is Activity && activityResultLauncher == null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) } else { intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS @@ -154,6 +158,9 @@ fun Context.openUrlInExternalApp( errorMessage: String = getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + if (this !is Activity) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } try { startActivity(intent) } catch (activityNotFoundException: ActivityNotFoundException) { diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt index 24296f8798..b0d3f3a339 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt @@ -31,9 +31,10 @@ class InviteFriendsUseCase @Inject constructor( private val stringProvider: StringProvider, private val matrixClient: MatrixClient, private val buildMeta: BuildMeta, + private val permalinkBuilder: PermalinkBuilder, ) { fun execute(activity: Activity) { - val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId) + val permalinkResult = permalinkBuilder.permalinkForUser(matrixClient.sessionId) permalinkResult.fold( onSuccess = { permalink -> val appName = buildMeta.applicationName diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index 338ad05349..63a5150531 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.modifiers.blurCompat import io.element.android.libraries.designsystem.modifiers.blurredShapeShadow @@ -171,6 +172,7 @@ internal fun ElementLogoAtomLargeNoBlurShadowPreview() = ElementPreview { ContentToPreview(ElementLogoAtomSize.Large, useBlurredShadow = false) } +@ExcludeFromCoverage @Composable private fun ContentToPreview(elementLogoAtomSize: ElementLogoAtomSize, useBlurredShadow: Boolean = true) { Box( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt index 1dc7230973..b90cf80aaa 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt @@ -32,6 +32,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize +import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text @@ -95,3 +96,14 @@ internal fun IconTitleSubtitleMoleculePreview() = ElementPreview { subTitle = "Subtitle", ) } + +@PreviewsDayNight +@Composable +internal fun IconTitleSubtitleMoleculeWithResIconPreview() = ElementPreview { + IconTitleSubtitleMolecule( + iconResourceId = CompoundDrawables.ic_compound_admin, + iconTint = Color.Black, + title = "Title", + subTitle = "Subtitle", + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt index f8e0b9baa8..5c25593184 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListItemMolecule.kt @@ -59,8 +59,8 @@ fun InfoListItemMolecule( color = backgroundColor, shape = backgroundShape, ) - .padding(vertical = 12.dp, horizontal = 20.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), + .padding(vertical = 12.dp, horizontal = 18.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { icon() message() diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt index 6b99537dc9..b2cf88b8bc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/HeaderFooterPage.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.designsystem.theme.components.Text /** * @param modifier Classical modifier. + * @param background optional background component. * @param topBar optional topBar. * @param header optional header. * @param footer optional footer. @@ -42,6 +43,7 @@ import io.element.android.libraries.designsystem.theme.components.Text @Composable fun HeaderFooterPage( modifier: Modifier = Modifier, + background: @Composable () -> Unit = {}, topBar: @Composable () -> Unit = {}, header: @Composable () -> Unit = {}, footer: @Composable () -> Unit = {}, @@ -51,25 +53,28 @@ fun HeaderFooterPage( modifier = modifier, topBar = topBar, ) { padding -> - Column( - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - .padding(all = 20.dp), - ) { - // Header - header() - // Content + Box { + background() Column( modifier = Modifier - .weight(1f) - .fillMaxWidth(), + .padding(all = 20.dp) + .padding(padding) + .consumeWindowInsets(padding) ) { - content() - } - // Footer - Box(modifier = Modifier.padding(horizontal = 16.dp)) { - footer() + // Header + header() + // Content + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + content() + } + // Footer + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + footer() + } } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/OnboardingBackground.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/OnboardingBackground.kt new file mode 100644 index 0000000000..5dbced7417 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/OnboardingBackground.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +/** + * Gradient background for FTUE (onboarding) screens. + */ +@Suppress("ModifierMissing") +@Composable +fun OnboardingBackground() { + Box(modifier = Modifier.fillMaxSize()) { + val isLightTheme = ElementTheme.isLightTheme + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(220.dp) + .align(Alignment.BottomCenter) + ) { + val gradientBrush = ShaderBrush( + LinearGradientShader( + from = Offset(0f, size.height / 2f), + to = Offset(size.width, size.height / 2f), + colors = listOf( + Color(0xFF0DBDA8), + if (isLightTheme) Color(0xC90D5CBD) else Color(0xFF0D5CBD), + ) + ) + ) + val eraseBrush = ShaderBrush( + LinearGradientShader( + from = Offset(size.width / 2f, 0f), + to = Offset(size.width / 2f, size.height * 2f), + colors = listOf( + Color(0xFF000000), + Color(0x00000000), + ) + ) + ) + drawWithLayer { + drawRect(brush = gradientBrush, size = size) + drawRect(brush = gradientBrush, size = size, blendMode = BlendMode.Overlay) + drawRect(brush = eraseBrush, size = size, blendMode = BlendMode.DstOut) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun OnboardingBackgroundPreview() { + ElementPreview { + OnboardingBackground() + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/PageTitle.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/PageTitle.kt index 9ff8ef38da..de1e25f5f3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/PageTitle.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/PageTitle.kt @@ -52,9 +52,7 @@ fun PageTitle( callToAction: @Composable (() -> Unit)? = null, ) { Column( - modifier = modifier - .fillMaxWidth() - .padding(bottom = 40.dp), + modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { BigIcon(style = iconStyle) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt index b1e7bc6be0..173716aee3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/Avatar.kt @@ -23,16 +23,21 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage +import coil.compose.AsyncImagePainter +import coil.compose.SubcomposeAsyncImage +import coil.compose.SubcomposeAsyncImageContent import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.preview.ElementThemedPreview @@ -71,16 +76,34 @@ private fun ImageAvatar( modifier: Modifier = Modifier, contentDescription: String? = null, ) { - AsyncImage( - model = avatarData, - onError = { - Timber.e(it.result.throwable, "Error loading avatar $it\n${it.result}") - }, - contentDescription = contentDescription, - contentScale = ContentScale.Crop, - placeholder = debugPlaceholderAvatar(), - modifier = modifier - ) + if (LocalInspectionMode.current) { + // For compose previews, use debugPlaceholderAvatar() + // instead of falling back to initials avatar on load failure + AsyncImage( + model = avatarData, + contentDescription = contentDescription, + placeholder = debugPlaceholderAvatar(), + modifier = modifier + ) + } else { + SubcomposeAsyncImage( + model = avatarData, + contentDescription = contentDescription, + contentScale = ContentScale.Crop, + modifier = modifier + ) { + when (val state = painter.state) { + is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent() + is AsyncImagePainter.State.Error -> { + SideEffect { + Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}") + } + InitialsAvatar(avatarData = avatarData) + } + else -> InitialsAvatar(avatarData = avatarData) + } + } + } } @Composable diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index 0451cb5839..2dc6c8875f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -51,5 +51,7 @@ enum class AvatarSize(val dp: Dp) { NotificationsOptIn(32.dp), - CustomRoomNotificationSetting(36.dp) + CustomRoomNotificationSetting(36.dp), + + RoomDirectoryItem(36.dp), } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt index 33f5e3b6bb..e8a859b54a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceText.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -162,6 +163,7 @@ internal fun PreferenceTextWithEndBadgeDarkPreview() = ElementPreviewDark { ContentToPreview(showEndBadge = true) } +@ExcludeFromCoverage @Composable private fun ContentToPreview(showEndBadge: Boolean) { Column( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt deleted file mode 100644 index 3f9ea0040e..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/CircularReveal.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.designsystem.modifiers - -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.updateTransition -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.drawscope.clipPath -import androidx.compose.ui.platform.debugInspectorInfo -import kotlin.math.sqrt - -// Note: these modifiers come from https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8 - -/** - * A modifier that clips the composable content using an animated circle. The circle will - * expand/shrink with an animation whenever [visible] changes. - * - * For more fine-grained control over the transition, see this method's overload, which allows passing - * a [State] object to control the progress of the reveal animation. - * - * By default, the circle is centered in the content, but custom positions may be specified using - * [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/ -fun Modifier.circularReveal( - visible: Boolean, - showScrim: Boolean = false, - revealFrom: Offset = Offset(0.5f, 0.5f), -): Modifier = composed( - factory = { - val factor = updateTransition(visible, label = "Visibility") - .animateFloat(label = "revealFactor") { if (it) 1f else 0f } - - circularReveal(factor, showScrim, revealFrom) - }, - inspectorInfo = debugInspectorInfo { - name = "circularReveal" - properties["visible"] = visible - properties["revealFrom"] = revealFrom - } -) - -/** - * A modifier that clips the composable content using a circular shape. The radius of the circle - * will be determined by the [transitionProgress]. - * - * The values of the progress should be between 0 and 1. - * - * By default, the circle is centered in the content, but custom positions may be specified using - * [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom). - * */ -fun Modifier.circularReveal( - transitionProgress: State, - showScrim: Boolean = false, - revealFrom: Offset = Offset(0.5f, 0.5f) -): Modifier { - return drawWithCache { - val path = Path() - val center = revealFrom.mapTo(size) - val radius = calculateRadius(revealFrom, size) - val scrimColor = if (showScrim) { - Color.Gray - } else { - Color.Transparent - } - - path.addOval(Rect(center, radius * transitionProgress.value)) - - onDrawWithContent { - if (showScrim) { - drawRect(scrimColor, alpha = transitionProgress.value * 0.75f) - } - clipPath(path) { this@onDrawWithContent.drawContent() } - } - } -} - -private fun Offset.mapTo(size: Size): Offset { - return Offset(x * size.width, y * size.height) -} - -private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) { - val x = (if (x > 0.5f) x else 1 - x) * size.width - val y = (if (y > 0.5f) y else 1 - y) * size.height - - sqrt(x * x + y * y) -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/SquareSizeModifier.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/SquareSizeModifier.kt new file mode 100644 index 0000000000..f2b7c49cf4 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/SquareSizeModifier.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.modifiers + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.InspectorValueInfo +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import kotlin.math.max +import kotlin.math.min + +/** + * Makes the content square in size. + * + * This is achieved by cropping incoming max constraints to the largest possible square size + * and measuring the content using resulting constraints. + * Next the size of layout is decided based on largest dimension of the measured content. + * Finally the content is placed inside the square layout according to specified [position]. + * + * If no square exists that falls within the size range of the incoming constraints, + * the content will be laid out as usual, as if the modifier was not applied. + * + * @param position The fraction of the content's position inside its square layout. + * It determines the point on the axis that was extended to make a square. + * Typically you'd want to use values between `0` and `1`, inclusive, where `0` + * will place the content at the "start" of the square, `0.5` in the middle, and `1` at the "end". + */ +@Stable +fun Modifier.squareSize( + position: Float = 0.5f, +): Modifier = + this.then( + when { + position == 0.5f -> SquareSizeCenter + else -> createSquareSizeModifier(position = position) + } + ) + +private val SquareSizeCenter = createSquareSizeModifier(position = 0.5f) + +private class SquareSizeModifier( + private val position: Float, + inspectorInfo: InspectorInfo.() -> Unit, +) : LayoutModifier, InspectorValueInfo(inspectorInfo) { + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + val maxSquare = min(constraints.maxWidth, constraints.maxHeight) + val minSquare = max(constraints.minWidth, constraints.minHeight) + val squareExists = minSquare <= maxSquare + + val resolvedConstraints = constraints + .takeUnless { squareExists } + ?: constraints.copy(maxWidth = maxSquare, maxHeight = maxSquare) + + val placeable = measurable.measure(resolvedConstraints) + + return if (squareExists) { + val size = max(placeable.width, placeable.height) + layout(size, size) { + val x = ((size - placeable.width) * position).toInt() + val y = ((size - placeable.height) * position).toInt() + placeable.placeRelative(x, y) + } + } else { + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (other !is SquareSizeModifier) return false + + if (position != other.position) return false + + return true + } + + override fun hashCode(): Int { + return position.hashCode() + } +} + +@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType") +private fun createSquareSizeModifier( + position: Float, +) = + SquareSizeModifier( + position = position, + inspectorInfo = debugInspectorInfo { + name = "squareSize" + properties["position"] = position + }, + ) + +@Preview +@Composable +internal fun SquareSizeModifierLargeWidthPreview() { + ElementPreview { + Box( + modifier = Modifier + .padding(32.dp) + .background(Color.Gray) + .squareSize(position = 0.25f) + ) { + Box( + modifier = Modifier + .background(Color.Black) + .size(100.dp, 10.dp) + ) + } + } +} + +@Preview +@Composable +internal fun SquareSizeModifierLargeHeightPreview() { + ElementPreview { + Box( + modifier = Modifier + .padding(32.dp) + .background(Color.Gray) + .squareSize(position = 0.75f) + ) { + Box( + modifier = Modifier + .background(Color.Black) + .size(10.dp, 100.dp) + ) + } + } +} + +@Preview +@Composable +internal fun SquareSizeModifierInsideSquarePreview() { + ElementPreview { + Box( + modifier = Modifier + .padding(32.dp) + .size(120.dp) + .background(Color.Gray), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .background(Color.Black) + .width(100.dp) + .squareSize(position = 0.75f) + ) + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt index 42289ad783..bddf614e68 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/SearchBar.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -269,6 +270,7 @@ internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview { @OptIn(ExperimentalMaterial3Api::class) @Composable +@ExcludeFromCoverage private fun ContentToPreview( query: String = "", active: Boolean = false, diff --git a/libraries/designsystem/src/main/res/drawable/ic_winner.xml b/libraries/designsystem/src/main/res/drawable/ic_winner.xml new file mode 100644 index 0000000000..20754ecfe8 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_winner.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt index e60840768e..ffaabb45ea 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -25,6 +25,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.SessionScope import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.impl.mode.RenderingMode +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem @@ -62,6 +63,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( private val roomMembershipContentFormatter: RoomMembershipContentFormatter, private val profileChangeContentFormatter: ProfileChangeContentFormatter, private val stateContentFormatter: StateContentFormatter, + private val permalinkParser: PermalinkParser ) : RoomLastMessageFormatter { companion object { // Max characters to display in the last message. This works around https://github.com/element-hq/element-x-android/issues/2105 @@ -121,7 +123,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( return "* $senderDisplayName ${messageType.body}" } is TextMessageType -> { - messageType.toPlainText() + messageType.toPlainText(permalinkParser) } is VideoMessageType -> { sp.getString(CommonStrings.common_video) diff --git a/libraries/eventformatter/impl/src/main/res/values-be/translations.xml b/libraries/eventformatter/impl/src/main/res/values-be/translations.xml index f7f1a652ba..00e6e1665e 100644 --- a/libraries/eventformatter/impl/src/main/res/values-be/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-be/translations.xml @@ -1,32 +1,32 @@ "(аватар таксама быў зменены)" - "%1$s змяніў аватар" + "%1$s змяніў(-ла) аватар" "Вы змянілі свой аватар" - "%1$s быў паніжаны да ўдзельніка" - "%1$s быў паніжаны да мадэратара" - "%1$s змяніў сваё адлюстраванае імя з %2$s на %3$s" + "%1$s быў паніжаны(-на) да ўдзельніка" + "%1$s быў паніжаны(-на) да мадэратара" + "%1$s змяніў(-ла) сваё адлюстраванае імя з %2$s на %3$s" "Вы змянілі сваё адлюстраванае імя з %1$s на %2$s" - "%1$s выдаліў сваё адлюстраванае імя (яно было %2$s)" + "%1$s выдаліў(-ла) сваё адлюстраванае імя (яно было %2$s)" "Вы выдалілі сваё адлюстраванае імя (яно было %1$s)" "%1$s усталявалі сваё адлюстраванае імя на %2$s" "Вы ўстанавілі адлюстраванае імя на %1$s" - "%1$s быў павышаны да адміністратара" - "%1$s быў павышаны да мадэратара" - "%1$s змяніў аватар пакоя" + "%1$s быў(-ла) павышаны(-на) да адміністратара" + "%1$s быў(-ла) павышаны(-на) да мадэратара" + "%1$s змяніў(-ла) аватар пакоя" "Вы змянілі аватар пакоя" "%1$s выдаліў(-ла) аватар пакоя" "Вы выдалілі аватар пакоя" - "%1$s заблакіраваў %2$s" + "%1$s заблакіраваў(-ла) %2$s" "Вы заблакіравалі %1$s" - "%1$s стварыў пакой" + "%1$s стварыў(-ла) пакой" "Вы стварылі пакой" - "%1$s запрасіў %2$s" + "%1$s запрасіў(-ла) %2$s" "%1$s прыняў(-ла) запрашэнне" "Вы прынялі запрашэнне" "Вы запрасілі %1$s" - "%1$s запрасіў вас" - "%1$s далучыўся да пакоя" + "%1$s запрасіў(-ла) вас" + "%1$s далучыўся(-лась) да пакоя" "Вы далучыліся да пакоя" "%1$s прасіў(-ла) далучыцца" "%1$s дазволіў(-ла) %2$s далучыцца" @@ -37,17 +37,17 @@ "%1$s адхіліў(-ла) ваш запыт на далучэнне" "%1$s больш не зацікаўлены(-на) у далучэнні" "Вы адмянілі запыт на далучэнне" - "%1$s выйшаў з пакоя" + "%1$s выйшаў(-ла) з пакоя" "Вы выйшлі з пакоя" - "%1$s змяніў назву пакоя на: %2$s" + "%1$s змяніў(-ла) назву пакоя на: %2$s" "Вы змянілі назву пакоя на: %1$s" "%1$s выдаліў(-ла) назву пакоя" "Вы выдалілі назву пакоя" - "%1$s не зрабіў ніякіх змен" + "%1$s не зрабіў(-ла) ніякіх змен" "Вы не зрабілі ніякіх змен" - "%1$s адхіліў запрашэнне" + "%1$s адхіліў(-ла) запрашэнне" "Вы адхілілі запрашэнне" - "%1$s выдаліў %2$s" + "%1$s выдаліў(-ла) %2$s" "Вы выдалілі %1$s" "%1$s адправіў(-ла) запрашэнне %2$s далучыцца да пакоя" "Вы адправілі запрашэнне %1$s далучыцца да пакоя" @@ -57,7 +57,7 @@ "Вы змянілі тэму на: %1$s" "%1$s выдаліў(-ла) тэму пакоя" "Вы выдалілі тэму пакоя" - "%1$s разблакіраваў %2$s" + "%1$s разблакіраваў(-ла) %2$s" "Вы разблакіравалі %1$s" "%1$s унеслі невядомую змену ў сяброўства" diff --git a/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml b/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml index 5aded12d62..5bcb239126 100644 --- a/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml @@ -16,7 +16,7 @@ "%1$s megváltoztatta a szoba profilképét" "Megváltoztatta a szoba profilképét" "%1$s eltávolította a szoba profilképét" - "Eltávolítottad a szoba profilképét" + "Eltávolította a szoba profilképét" "%1$s kitiltotta: %2$s" "Kitiltotta: %1$s" "%1$s létrehozta a szobát" diff --git a/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml b/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml index fab8a16e94..eccaa950eb 100644 --- a/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-uk/translations.xml @@ -3,16 +3,16 @@ "(аватар теж було змінено)" "%1$s змінив (-ла) свій аватар" "Ви змінили свій аватар" - "%1$s був понижений до члена" - "%1$s був понижений до модератора" + "%1$s понижено до учасника" + "%1$s понижено до модератора" "%1$s змінив (-ла) своє імʼя з %2$s на %3$s" "Ви змінили своє ім\'я з %1$s на %2$s" "%1$s видалив (-ла) своє ім\'я (було %2$s)" "Ви видалили своє ім\'я (було%1$s)" "%1$s змінив (-ла) своє ім\'я на %2$s" "Ви змінили своє імʼя на %1$s" - "%1$s був підвищений до адміністратора" - "%1$s був підвищений до модератора" + "%1$s підвищено до адміністратора" + "%1$s підвищено до модератора" "%1$s змінив (-ла) аватар кімнати" "Ви змінили аватар кімнати" "%1$s видалив (-ла) аватар кімнати" diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt index a0a17b3465..158eeec20d 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt @@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem @@ -78,7 +79,8 @@ class DefaultRoomLastMessageFormatterTest { sp = AndroidStringProvider(context.resources), roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), - stateContentFormatter = StateContentFormatter(stringProvider) + stateContentFormatter = StateContentFormatter(stringProvider), + permalinkParser = FakePermalinkParser(), ) } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index a242d014d1..765e97f851 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -82,11 +82,11 @@ enum class FeatureFlags( defaultValue = true, isFinished = false, ), - RoomModeration( - key = "feature.roomModeration", - title = "Room moderation", - description = "Add moderation features to the room for users with permissions", - defaultValue = true, + RoomDirectorySearch( + key = "feature.roomdirectorysearch", + title = "Room directory search", + description = "Allow user to search for public rooms in their homeserver", + defaultValue = false, isFinished = false, - ), + ) } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 4e7032a313..43fc0f0823 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -41,7 +41,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.Mentions -> true FeatureFlags.MarkAsUnread -> true FeatureFlags.RoomListFilters -> true - FeatureFlags.RoomModeration -> false + FeatureFlags.RoomDirectorySearch -> false } } else { false diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 9304634be2..6219296732 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -34,7 +34,6 @@ anvil { } dependencies { - implementation(projects.appconfig) implementation(projects.libraries.di) implementation(libs.dagger) implementation(projects.libraries.androidutils) @@ -45,7 +44,5 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) - testImplementation(libs.test.robolectric) - testImplementation(projects.tests.testutils) testImplementation(projects.libraries.matrix.test) } 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 955c8d72fa..fa5f083722 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 @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults @@ -58,12 +59,14 @@ interface MatrixClient : Closeable { suspend fun setDisplayName(displayName: String): Result suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result + suspend fun joinRoom(roomId: RoomId): Result fun syncService(): SyncService fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService fun notificationService(): NotificationService fun notificationSettingsService(): NotificationSettingsService fun encryptionService(): EncryptionService + fun roomDirectoryService(): RoomDirectoryService suspend fun getCacheSize(): Long /** @@ -88,4 +91,7 @@ interface MatrixClient : Closeable { fun roomMembershipObserver(): RoomMembershipObserver fun isMe(userId: UserId?) = userId == sessionId + + suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result + suspend fun getRecentlyVisitedRooms(): Result> } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt index 4ae7b7dc94..c525bd4c00 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt @@ -17,40 +17,17 @@ package io.element.android.libraries.matrix.api.permalink import android.net.Uri -import io.element.android.appconfig.MatrixConfiguration /** * Mapping of an input URI to a matrix.to compliant URI. */ -object MatrixToConverter { +interface MatrixToConverter { /** * Try to convert a URL from an element web instance or from a client permalink to a matrix.to url. - * To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS]. * Examples: * - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org * - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org * - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org */ - fun convert(uri: Uri): Uri? { - val uriString = uri.toString() - val baseUrl = MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL - - return when { - // URL is already a matrix.to - uriString.startsWith(baseUrl) -> uri - // Web or client url - SUPPORTED_PATHS.any { it in uriString } -> { - val path = SUPPORTED_PATHS.first { it in uriString } - Uri.parse(baseUrl + uriString.substringAfter(path)) - } - // URL is not supported - else -> null - } - } - - private val SUPPORTED_PATHS = listOf( - "/#/room/", - "/#/user/", - "/#/group/" - ) + fun convert(uri: Uri): Uri? } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt index c46e15db3b..14c29f2de5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt @@ -16,70 +16,13 @@ package io.element.android.libraries.matrix.api.permalink -import io.element.android.appconfig.MatrixConfiguration -import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId -object PermalinkBuilder { - private const val ROOM_PATH = "room/" - private const val USER_PATH = "user/" - - private val permalinkBaseUrl get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL).also { - var baseUrl = it - if (!baseUrl.endsWith("/")) { - baseUrl += "/" - } - if (!baseUrl.endsWith("/#/")) { - baseUrl += "/#/" - } - } - - fun permalinkForUser(userId: UserId): Result { - return if (MatrixPatterns.isUserId(userId.value)) { - val url = buildString { - append(permalinkBaseUrl) - if (!isMatrixTo()) { - append(USER_PATH) - } - append(userId.value) - } - Result.success(url) - } else { - Result.failure(PermalinkBuilderError.InvalidUserId) - } - } - - fun permalinkForRoomAlias(roomAlias: String): Result { - return if (MatrixPatterns.isRoomAlias(roomAlias)) { - Result.success(permalinkForRoomAliasOrId(roomAlias)) - } else { - Result.failure(PermalinkBuilderError.InvalidRoomAlias) - } - } - - fun permalinkForRoomId(roomId: RoomId): Result { - return if (MatrixPatterns.isRoomId(roomId.value)) { - Result.success(permalinkForRoomAliasOrId(roomId.value)) - } else { - Result.failure(PermalinkBuilderError.InvalidRoomId) - } - } - - private fun permalinkForRoomAliasOrId(value: String): String { - val id = escapeId(value) - return buildString { - append(permalinkBaseUrl) - if (!isMatrixTo()) { - append(ROOM_PATH) - } - append(id) - } - } - - private fun escapeId(value: String) = value.replace("/", "%2F") - - private fun isMatrixTo(): Boolean = permalinkBaseUrl.startsWith(MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL) +interface PermalinkBuilder { + fun permalinkForUser(userId: UserId): Result + fun permalinkForRoomAlias(roomAlias: String): Result + fun permalinkForRoomId(roomId: RoomId): Result } sealed class PermalinkBuilderError : Throwable() { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt index 3b90aee1be..463f8fb32d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt @@ -17,11 +17,6 @@ package io.element.android.libraries.matrix.api.permalink import android.net.Uri -import android.net.UrlQuerySanitizer -import io.element.android.libraries.matrix.api.core.MatrixPatterns -import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber -import java.net.URLDecoder /** * This class turns a uri to a [PermalinkData]. @@ -29,121 +24,15 @@ import java.net.URLDecoder * or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) * or client permalinks (e.g. user/@chagai95:matrix.org) */ -object PermalinkParser { +interface PermalinkParser { /** * Turns a uri string to a [PermalinkData]. */ - fun parse(uriString: String): PermalinkData { - val uri = Uri.parse(uriString) - return parse(uri) - } + fun parse(uriString: String): PermalinkData /** * Turns a uri to a [PermalinkData]. * https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md */ - fun parse(uri: Uri): PermalinkData { - // the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the - // mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid - // so convert URI to matrix.to to simplify parsing process - val matrixToUri = MatrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) - - // We can't use uri.fragment as it is decoding to early and it will break the parsing - // of parameters that represents url (like signurl) - val fragment = matrixToUri.toString().substringAfter("#") // uri.fragment - if (fragment.isEmpty()) { - return PermalinkData.FallbackLink(uri) - } - val safeFragment = fragment.substringBefore('?') - val viaQueryParameters = fragment.getViaParameters() - - // we are limiting to 2 params - val params = safeFragment - .split(MatrixPatterns.SEP_REGEX) - .filter { it.isNotEmpty() } - .take(2) - - val decodedParams = params - .map { URLDecoder.decode(it, "UTF-8") } - - val identifier = params.getOrNull(0) - val decodedIdentifier = decodedParams.getOrNull(0) - val extraParameter = decodedParams.getOrNull(1) - return when { - identifier.isNullOrEmpty() || decodedIdentifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) - MatrixPatterns.isUserId(decodedIdentifier) -> PermalinkData.UserLink(userId = decodedIdentifier) - MatrixPatterns.isRoomId(decodedIdentifier) -> { - handleRoomIdCase(fragment, decodedIdentifier, matrixToUri, extraParameter, viaQueryParameters) - } - MatrixPatterns.isRoomAlias(decodedIdentifier) -> { - PermalinkData.RoomLink( - roomIdOrAlias = decodedIdentifier, - isRoomAlias = true, - eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, - viaParameters = viaQueryParameters.toImmutableList() - ) - } - else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier)) - } - } - - private fun handleRoomIdCase(fragment: String, identifier: String, uri: Uri, extraParameter: String?, viaQueryParameters: List): PermalinkData { - // Can't rely on built in parsing because it's messing around the signurl - val paramList = safeExtractParams(fragment) - val signUrl = paramList.firstOrNull { it.first == "signurl" }?.second - val email = paramList.firstOrNull { it.first == "email" }?.second - return if (signUrl.isNullOrEmpty().not() && email.isNullOrEmpty().not()) { - try { - val signValidUri = Uri.parse(signUrl) - val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException("missing `authority`") - val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException("missing `token`") - val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException("missing `private_key`") - PermalinkData.RoomEmailInviteLink( - roomId = identifier, - email = email!!, - signUrl = signUrl!!, - roomName = paramList.firstOrNull { it.first == "room_name" }?.second, - inviterName = paramList.firstOrNull { it.first == "inviter_name" }?.second, - roomAvatarUrl = paramList.firstOrNull { it.first == "room_avatar_url" }?.second, - roomType = paramList.firstOrNull { it.first == "room_type" }?.second, - identityServer = identityServerHost, - token = token, - privateKey = privateKey - ) - } catch (failure: Throwable) { - Timber.i("## Permalink: Failed to parse permalink $signUrl") - PermalinkData.FallbackLink(uri) - } - } else { - PermalinkData.RoomLink( - roomIdOrAlias = identifier, - isRoomAlias = false, - eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, - viaParameters = viaQueryParameters.toImmutableList() - ) - } - } - - private fun safeExtractParams(fragment: String) = - fragment.substringAfter("?").split('&').mapNotNull { - val splitNameValue = it.split("=") - if (splitNameValue.size == 2) { - Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8")) - } else { - null - } - } - - private fun String.getViaParameters(): List { - return runCatching { - UrlQuerySanitizer(this) - .parameterList - .filter { - it.mParameter == "via" - } - .map { - URLDecoder.decode(it.mValue, "UTF-8") - } - }.getOrDefault(emptyList()) - } + fun parse(uri: Uri): PermalinkData } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 7e407aad4f..07b5b310bc 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -35,13 +35,8 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import java.io.Closeable import java.io.File @@ -55,6 +50,7 @@ interface MatrixRoom : Closeable { val topic: String? val avatarUrl: String? val isEncrypted: Boolean + val isSpace: Boolean val isDirect: Boolean val isPublic: Boolean val activeMemberCount: Long @@ -86,6 +82,12 @@ interface MatrixRoom : Closeable { */ suspend fun updateMembers() + /** + * Get the members of the room. Note: generally this should not be used, please use + * [membersStateFlow] and [updateMembers] instead. + */ + suspend fun getMembers(limit: Int = 5): Result> + /** * Will return an updated member or an error. */ @@ -182,18 +184,6 @@ interface MatrixRoom : Closeable { suspend fun canUserJoinCall(userId: UserId): Result = canUserSendState(userId, StateEventType.CALL_MEMBER) - fun usersWithRole(role: RoomMember.Role): Flow> { - return roomInfoFlow - .map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } } - .distinctUntilChanged() - .combine(membersStateFlow) { powerLevels, membersState -> - membersState.roomMembers() - .orEmpty() - .filter { powerLevels.containsKey(it.userId) } - .toPersistentList() - } - } - suspend fun updateAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result @@ -327,5 +317,12 @@ interface MatrixRoom : Closeable { */ fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result + /** + * Get the permalink for the provided [eventId]. + * @param eventId The event id to get the permalink for. + * @return The permalink, or a failure. + */ + suspend fun getPermalinkFor(eventId: EventId): Result + override fun close() = destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt index 759b0f46cb..13f19fe0e0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt @@ -35,3 +35,7 @@ fun MatrixRoomMembersState.roomMembers(): List? { else -> null } } + +fun MatrixRoomMembersState.joinedRoomMembers(): List { + return roomMembers().orEmpty().filter { it.membership == RoomMembershipState.JOIN } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index f6609a2bce..4415e51327 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.api.room import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.MatrixUser data class RoomMember( val userId: UserId, @@ -78,3 +79,9 @@ enum class RoomMembershipState { fun RoomMember.getBestName(): String { return displayName?.takeIf { it.isNotEmpty() } ?: userId.value } + +fun RoomMember.toMatrixUser() = MatrixUser( + userId = userId, + displayName = displayName, + avatarUrl = avatarUrl, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt new file mode 100644 index 0000000000..2c17718ccf --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomMembersWithRole.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.room.powerlevels + +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.joinedRoomMembers +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** + * Return a flow of the list of room members who are still in the room (with membership == RoomMembershipState.JOIN) + * and who have the given role. + */ +fun MatrixRoom.usersWithRole(role: RoomMember.Role): Flow> { + return roomInfoFlow + .map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } } + .combine(membersStateFlow) { powerLevels, membersState -> + membersState.joinedRoomMembers() + .filter { powerLevels.containsKey(it.userId) } + .toPersistentList() + } + .distinctUntilChanged() +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt index f2577fcb6b..ef4e6f747e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt @@ -42,7 +42,7 @@ suspend fun MatrixRoom.canInvite(): Result = canUserInvite(sessionId) suspend fun MatrixRoom.canKick(): Result = canUserKick(sessionId) /** - * Shortcut for calling [MatrixRoom.canBanUser] with our own user. + * Shortcut for calling [MatrixRoom.canUserBan] with our own user. */ suspend fun MatrixRoom.canBan(): Result = canUserBan(sessionId) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt new file mode 100644 index 0000000000..c2fb147aa0 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/recent/RecentDirectRoom.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.room.recent + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.toMatrixUser +import io.element.android.libraries.matrix.api.user.MatrixUser +import kotlinx.coroutines.flow.first + +private const val MAX_RECENT_DIRECT_ROOMS_TO_RETURN = 5 + +data class RecentDirectRoom( + val roomId: RoomId, + val matrixUser: MatrixUser, +) + +suspend fun MatrixClient.getRecentDirectRooms( + maxNumberOfResults: Int = MAX_RECENT_DIRECT_ROOMS_TO_RETURN, +): List { + val result = mutableListOf() + val foundUserIds = mutableSetOf() + getRecentlyVisitedRooms().getOrNull()?.let { roomIds -> + roomIds + .mapNotNull { roomId -> getRoom(roomId) } + .filter { it.isDm && it.isJoined() } + .map { room -> + val otherUser = room.getMembers().getOrNull() + ?.firstOrNull { it.userId != sessionId } + ?.takeIf { foundUserIds.add(it.userId) } + ?.toMatrixUser() + if (otherUser != null) { + result.add( + RecentDirectRoom(room.roomId, otherUser) + ) + // Return early to avoid useless computation + if (result.size >= maxNumberOfResults) { + return@map + } + } + } + } + return result +} + +suspend fun MatrixRoom.isJoined(): Boolean { + return roomInfoFlow.first().currentUserMembership == CurrentUserMembership.JOINED +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt new file mode 100644 index 0000000000..78d6cb0c94 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDescription.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.roomdirectory + +import io.element.android.libraries.matrix.api.core.RoomId + +data class RoomDescription( + val roomId: RoomId, + val name: String?, + val topic: String?, + val alias: String?, + val avatarUrl: String?, + val joinRule: JoinRule, + val isWorldReadable: Boolean, + val numberOfMembers: Long +) { + enum class JoinRule { + PUBLIC, + KNOCK, + UNKNOWN + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt new file mode 100644 index 0000000000..2311c5afee --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.roomdirectory + +import kotlinx.coroutines.flow.Flow + +interface RoomDirectoryList { + suspend fun filter(filter: String?, batchSize: Int): Result + suspend fun loadMore(): Result + val state: Flow + + data class State( + val hasMoreToLoad: Boolean, + val items: List, + ) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt new file mode 100644 index 0000000000..26df48be71 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.roomdirectory + +import kotlinx.coroutines.CoroutineScope + +interface RoomDirectoryService { + fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt index 00fd9dc561..b82dd23188 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt @@ -21,6 +21,17 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface SessionVerificationService { + /** + * This flow stores the local verification status of the current session. + * + * We should ideally base the verified status in the Rust SDK info, but there are several issues with that approach: + * + * - The SDK takes a while to report this value, resulting in a delay of 1-2s in displaying the UI. + * - We need to add a 'Skip' option for testing purposes, which would not be possible if we relied only on the SDK. + * - The SDK sometimes doesn't report the verification state if there is no network connection when the app boots. + */ + val needsVerificationFlow: StateFlow + /** * State of the current verification flow ([VerificationFlowState.Initial] if not started). */ @@ -72,6 +83,11 @@ interface SessionVerificationService { * Returns the verification service state to the initial step. */ suspend fun reset() + + /** + * Saves the current session state as [verified]. + */ + suspend fun saveVerifiedState(verified: Boolean) } /** Verification status of the current session. */ @@ -85,6 +101,9 @@ sealed interface SessionVerifiedStatus { /** Verified session status. */ data object Verified : SessionVerifiedStatus + + /** Returns whether the session is [Verified]. */ + fun isVerified(): Boolean = this is Verified } /** States produced by the [SessionVerificationService]. */ diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index d50bad6696..523ea2fda1 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { } else { debugImplementation(libs.matrix.sdk) } + implementation(projects.appconfig) implementation(projects.libraries.di) implementation(projects.libraries.androidutils) implementation(projects.libraries.network) @@ -52,8 +53,10 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.analytics.test) + testImplementation(projects.tests.testutils) testImplementation(libs.coroutines.test) testImplementation(libs.test.turbine) } 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 31e439bbec..f67548afae 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 @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.impl +import io.element.android.appconfig.TimelineConfig import io.element.android.libraries.androidutils.file.getSizeOfFiles import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -35,9 +36,11 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.awaitLoaded import io.element.android.libraries.matrix.api.sync.SyncService +import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -53,6 +56,8 @@ import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.libraries.matrix.impl.room.RustMatrixRoom +import io.element.android.libraries.matrix.impl.room.map +import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService import io.element.android.libraries.matrix.impl.roomlist.fullRoomWithTimeline @@ -71,6 +76,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -79,8 +85,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -94,13 +99,14 @@ import org.matrix.rustcomponents.sdk.NotificationProcessSetup import org.matrix.rustcomponents.sdk.PowerLevels import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListItem -import org.matrix.rustcomponents.sdk.StateEventType import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility @@ -124,11 +130,6 @@ class RustMatrixClient( private val innerRoomListService = syncService.roomListService() private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope) - private val verificationService = RustSessionVerificationService( - client = client, - syncService = rustSyncService, - sessionCoroutineScope = sessionCoroutineScope, - ).apply { start() } private val pushersService = RustPushersService( client = client, dispatchers = dispatchers, @@ -149,7 +150,14 @@ class RustMatrixClient( syncService = rustSyncService, sessionCoroutineScope = sessionCoroutineScope, dispatchers = dispatchers, + sessionStore = sessionStore, ) + + private val roomDirectoryService = RustRoomDirectoryService( + client = client, + sessionDispatcher = sessionDispatcher, + ) + private val sessionDirectoryNameProvider = SessionDirectoryNameProvider() private val isLoggingOut = AtomicBoolean(false) @@ -170,6 +178,7 @@ class RustMatrixClient( isTokenValid = false, loginType = existingData.loginType, passphrase = existingData.passphrase, + needsVerification = existingData.needsVerification, ) sessionStore.updateData(newData) Timber.d("Removed session data with token: '...$anonymizedToken'.") @@ -197,6 +206,7 @@ class RustMatrixClient( isTokenValid = true, loginType = existingData.loginType, passphrase = existingData.passphrase, + needsVerification = existingData.needsVerification, ) sessionStore.updateData(newData) Timber.d("Saved new session data with token: '...$anonymizedToken'.") @@ -208,42 +218,38 @@ class RustMatrixClient( } } - private val rustRoomListService: RoomListService = - RustRoomListService( + override val roomListService: RoomListService = RustRoomListService( + innerRoomListService = innerRoomListService, + sessionCoroutineScope = sessionCoroutineScope, + sessionDispatcher = sessionDispatcher, + roomListFactory = RoomListFactory( innerRoomListService = innerRoomListService, sessionCoroutineScope = sessionCoroutineScope, - sessionDispatcher = sessionDispatcher, - roomListFactory = RoomListFactory( - innerRoomListService = innerRoomListService, - sessionCoroutineScope = sessionCoroutineScope, - ), - ) - - private val eventFilters = TimelineEventTypeFilter.exclude( - listOf( - StateEventType.ROOM_ALIASES, - StateEventType.ROOM_CANONICAL_ALIAS, - StateEventType.ROOM_GUEST_ACCESS, - StateEventType.ROOM_HISTORY_VISIBILITY, - StateEventType.ROOM_JOIN_RULES, - StateEventType.ROOM_PINNED_EVENTS, - StateEventType.ROOM_POWER_LEVELS, - StateEventType.ROOM_SERVER_ACL, - StateEventType.ROOM_TOMBSTONE, - StateEventType.SPACE_CHILD, - StateEventType.SPACE_PARENT, - StateEventType.POLICY_RULE_ROOM, - StateEventType.POLICY_RULE_SERVER, - StateEventType.POLICY_RULE_USER, - ).map(FilterTimelineEventType::State) + ), ) - override val roomListService: RoomListService - get() = rustRoomListService + private val verificationService = RustSessionVerificationService( + client = client, + isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running }, + sessionCoroutineScope = sessionCoroutineScope, + sessionStore = sessionStore, + ) - private val rustMediaLoader = RustMediaLoader(baseCacheDirectory, dispatchers, client) - override val mediaLoader: MatrixMediaLoader - get() = rustMediaLoader + private val eventFilters = TimelineConfig.excludedEvents + .takeIf { it.isNotEmpty() } + ?.let { listStateEventType -> + TimelineEventTypeFilter.exclude( + listStateEventType.map { stateEventType -> + FilterTimelineEventType.State(stateEventType.map()) + } + ) + } + + override val mediaLoader: MatrixMediaLoader = RustMediaLoader( + baseCacheDirectory = baseCacheDirectory, + dispatchers = dispatchers, + innerClient = client, + ) private val roomMembershipObserver = RoomMembershipObserver() @@ -273,11 +279,6 @@ class RustMatrixClient( .stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf()) init { - roomListService.state.onEach { state -> - if (state == RoomListService.State.Running) { - setupVerificationControllerIfNeeded() - } - }.launchIn(sessionCoroutineScope) sessionCoroutineScope.launch { // Force a refresh of the profile getUserProfile() @@ -311,6 +312,22 @@ class RustMatrixClient( } } + /** + * Wait for the room to be available in the room list. + * @param roomId the room id to wait for + * @param timeout the timeout to wait for the room to be available + * @throws TimeoutCancellationException if the room is not available after the timeout + */ + private suspend fun awaitRoom(roomId: RoomId, timeout: Duration) { + withTimeout(timeout) { + roomListService.allRooms.summaries + .filter { roomSummaries -> + roomSummaries.map { it.identifier() }.contains(roomId.value) + } + .first() + } + } + private suspend fun pairOfRoom(roomId: RoomId): Pair? { val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value) val fullRoom = cachedRoomListItem?.fullRoomWithTimeline(filter = eventFilters) @@ -360,14 +377,11 @@ class RustMatrixClient( powerLevelContentOverride = defaultRoomCreationPowerLevels, ) val roomId = RoomId(client.createRoom(rustParams)) - - // Wait to receive the room back from the sync - withTimeout(30_000L) { - roomListService.allRooms.summaries - .filter { roomSummaries -> - roomSummaries.map { it.identifier() }.contains(roomId.value) - } - .first() + // Wait to receive the room back from the sync but do not returns failure if it fails. + try { + awaitRoom(roomId, 30.seconds) + } catch (e: Exception) { + Timber.e(e, "Timeout waiting for the room to be available in the room list") } roomId } @@ -416,6 +430,30 @@ class RustMatrixClient( runCatching { client.removeAvatar() } } + override suspend fun joinRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { + runCatching { + client.joinRoomById(roomId.value).destroy() + try { + awaitRoom(roomId, 10.seconds) + } catch (e: Exception) { + Timber.e(e, "Timeout waiting for the room to be available in the room list") + } + roomId + } + } + + override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result = withContext(sessionDispatcher) { + runCatching { + client.trackRecentlyVisitedRoom(roomId.value) + } + } + + override suspend fun getRecentlyVisitedRooms(): Result> = withContext(sessionDispatcher) { + runCatching { + client.getRecentlyVisitedRooms().map(::RoomId) + } + } + override fun syncService(): SyncService = rustSyncService override fun sessionVerificationService(): SessionVerificationService = verificationService @@ -428,6 +466,8 @@ class RustMatrixClient( override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService + override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService + override fun close() { sessionCoroutineScope.cancel() clientDelegateTaskHandle?.cancelAndDestroy() @@ -462,6 +502,7 @@ class RustMatrixClient( ignoreSdkError: Boolean, ): String? { var result: String? = null + syncService.stop() withContext(sessionDispatcher) { if (doRequest) { try { @@ -497,16 +538,6 @@ class RustMatrixClient( } } - private fun setupVerificationControllerIfNeeded() { - if (verificationService.verificationController == null) { - try { - verificationService.verificationController = client.getSessionVerificationController() - } catch (e: Throwable) { - Timber.e(e, "Could not start verification service. Will try again on the next sliding sync update.") - } - } - } - override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver private suspend fun File.getCacheSize( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 29dd327ae2..ad34e30d18 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -148,6 +148,7 @@ class RustMatrixAuthenticationService @Inject constructor( isTokenValid = true, loginType = LoginType.PASSWORD, passphrase = pendingPassphrase, + needsVerification = true, ) } sessionStore.storeData(sessionData) @@ -195,7 +196,8 @@ class RustMatrixAuthenticationService @Inject constructor( it.session().toSessionData( isTokenValid = true, loginType = LoginType.OIDC, - passphrase = pendingPassphrase + passphrase = pendingPassphrase, + needsVerification = true, ) } pendingOidcAuthenticationData?.close() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 17ea8ee444..bf5c4c601f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.verification.SessionVerificationService import kotlinx.coroutines.CoroutineScope @@ -68,4 +69,9 @@ object SessionMatrixModule { fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope { return matrixClient.sessionCoroutineScope } + + @Provides + fun providesRoomDirectoryService(matrixClient: MatrixClient): RoomDirectoryService { + return matrixClient.roomDirectoryService() + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index f5a6390989..25881c2174 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.impl.sync.RustSyncService +import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.currentCoroutineContext @@ -48,10 +49,11 @@ import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecover import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException internal class RustEncryptionService( - client: Client, + private val client: Client, syncService: RustSyncService, sessionCoroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, + private val sessionStore: SessionStore, ) : EncryptionService { private val service: Encryption = client.encryption() @@ -186,6 +188,9 @@ internal class RustEncryptionService( override suspend fun recover(recoveryKey: String): Result = withContext(dispatchers.io) { runCatching { service.recover(recoveryKey) + val existingSession = sessionStore.getSession(client.userId()) + ?: error("Failed to save verification state. No session with id ${client.userId()}") + sessionStore.updateData(existingSession.copy(needsVerification = false)) }.mapFailure { it.mapRecoveryException() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt index aea838b705..3c1e3c40ec 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt @@ -25,6 +25,7 @@ internal fun Session.toSessionData( isTokenValid: Boolean, loginType: LoginType, passphrase: String?, + needsVerification: Boolean, ) = SessionData( userId = userId, deviceId = deviceId, @@ -37,4 +38,5 @@ internal fun Session.toSessionData( isTokenValid = isTokenValid, loginType = loginType, passphrase = passphrase, + needsVerification = needsVerification, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt new file mode 100644 index 0000000000..a5271c0b22 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.permalink + +import android.net.Uri +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.MatrixConfiguration +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.permalink.MatrixToConverter +import javax.inject.Inject + +/** + * Mapping of an input URI to a matrix.to compliant URI. + */ +@ContributesBinding(AppScope::class) +class DefaultMatrixToConverter @Inject constructor() : MatrixToConverter { + /** + * Try to convert a URL from an element web instance or from a client permalink to a matrix.to url. + * To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS]. + * Examples: + * - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org + * - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org + * - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org + */ + override fun convert(uri: Uri): Uri? { + val uriString = uri.toString() + val baseUrl = MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL + + return when { + // URL is already a matrix.to + uriString.startsWith(baseUrl) -> uri + // Web or client url + SUPPORTED_PATHS.any { it in uriString } -> { + val path = SUPPORTED_PATHS.first { it in uriString } + Uri.parse(baseUrl + uriString.substringAfter(path)) + } + // URL is not supported + else -> null + } + } + + companion object { + val SUPPORTED_PATHS = listOf( + "/#/room/", + "/#/user/", + "/#/group/" + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt new file mode 100644 index 0000000000..ae30c94c9c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.permalink + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.MatrixConfiguration +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.MatrixPatterns +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder +import io.element.android.libraries.matrix.api.permalink.PermalinkBuilderError +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder { + private val permalinkBaseUrl + get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL).also { + var baseUrl = it + if (!baseUrl.endsWith("/")) { + baseUrl += "/" + } + if (!baseUrl.endsWith("/#/")) { + baseUrl += "/#/" + } + } + + override fun permalinkForUser(userId: UserId): Result { + return if (MatrixPatterns.isUserId(userId.value)) { + val url = buildString { + append(permalinkBaseUrl) + if (!isMatrixTo()) { + append(USER_PATH) + } + append(userId.value) + } + Result.success(url) + } else { + Result.failure(PermalinkBuilderError.InvalidUserId) + } + } + + override fun permalinkForRoomAlias(roomAlias: String): Result { + return if (MatrixPatterns.isRoomAlias(roomAlias)) { + Result.success(permalinkForRoomAliasOrId(roomAlias)) + } else { + Result.failure(PermalinkBuilderError.InvalidRoomAlias) + } + } + + override fun permalinkForRoomId(roomId: RoomId): Result { + return if (MatrixPatterns.isRoomId(roomId.value)) { + Result.success(permalinkForRoomAliasOrId(roomId.value)) + } else { + Result.failure(PermalinkBuilderError.InvalidRoomId) + } + } + + private fun permalinkForRoomAliasOrId(value: String): String { + val id = escapeId(value) + return buildString { + append(permalinkBaseUrl) + if (!isMatrixTo()) { + append(ROOM_PATH) + } + append(id) + } + } + + private fun escapeId(value: String) = value.replace("/", "%2F") + + private fun isMatrixTo(): Boolean = permalinkBaseUrl.startsWith(MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL) + + companion object { + private const val ROOM_PATH = "room/" + private const val USER_PATH = "user/" + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt new file mode 100644 index 0000000000..ab91a89af9 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.permalink + +import android.net.Uri +import android.net.UrlQuerySanitizer +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.MatrixPatterns +import io.element.android.libraries.matrix.api.permalink.MatrixToConverter +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber +import java.net.URLDecoder +import javax.inject.Inject + +/** + * This class turns a uri to a [PermalinkData]. + * element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks + * or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) + * or client permalinks (e.g. user/@chagai95:matrix.org) + */ +@ContributesBinding(AppScope::class) +class DefaultPermalinkParser @Inject constructor( + private val matrixToConverter: MatrixToConverter +) : PermalinkParser { + /** + * Turns a uri string to a [PermalinkData]. + */ + override fun parse(uriString: String): PermalinkData { + val uri = Uri.parse(uriString) + return parse(uri) + } + + /** + * Turns a uri to a [PermalinkData]. + * https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md + */ + override fun parse(uri: Uri): PermalinkData { + // the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the + // mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid + // so convert URI to matrix.to to simplify parsing process + val matrixToUri = matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) + + // We can't use uri.fragment as it is decoding to early and it will break the parsing + // of parameters that represents url (like signurl) + val fragment = matrixToUri.toString().substringAfter("#") // uri.fragment + if (fragment.isEmpty()) { + return PermalinkData.FallbackLink(uri) + } + val safeFragment = fragment.substringBefore('?') + val viaQueryParameters = fragment.getViaParameters() + + // we are limiting to 2 params + val params = safeFragment + .split(MatrixPatterns.SEP_REGEX) + .filter { it.isNotEmpty() } + .take(2) + + val decodedParams = params + .map { URLDecoder.decode(it, "UTF-8") } + + val identifier = params.getOrNull(0) + val decodedIdentifier = decodedParams.getOrNull(0) + val extraParameter = decodedParams.getOrNull(1) + return when { + identifier.isNullOrEmpty() || decodedIdentifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) + MatrixPatterns.isUserId(decodedIdentifier) -> PermalinkData.UserLink(userId = decodedIdentifier) + MatrixPatterns.isRoomId(decodedIdentifier) -> { + handleRoomIdCase(fragment, decodedIdentifier, matrixToUri, extraParameter, viaQueryParameters) + } + MatrixPatterns.isRoomAlias(decodedIdentifier) -> { + PermalinkData.RoomLink( + roomIdOrAlias = decodedIdentifier, + isRoomAlias = true, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, + viaParameters = viaQueryParameters.toImmutableList() + ) + } + else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier)) + } + } + + private fun handleRoomIdCase(fragment: String, identifier: String, uri: Uri, extraParameter: String?, viaQueryParameters: List): PermalinkData { + // Can't rely on built in parsing because it's messing around the signurl + val paramList = safeExtractParams(fragment) + val signUrl = paramList.firstOrNull { it.first == "signurl" }?.second + val email = paramList.firstOrNull { it.first == "email" }?.second + return if (signUrl.isNullOrEmpty().not() && email.isNullOrEmpty().not()) { + try { + val signValidUri = Uri.parse(signUrl) + val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException("missing `authority`") + val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException("missing `token`") + val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException("missing `private_key`") + PermalinkData.RoomEmailInviteLink( + roomId = identifier, + email = email!!, + signUrl = signUrl!!, + roomName = paramList.firstOrNull { it.first == "room_name" }?.second, + inviterName = paramList.firstOrNull { it.first == "inviter_name" }?.second, + roomAvatarUrl = paramList.firstOrNull { it.first == "room_avatar_url" }?.second, + roomType = paramList.firstOrNull { it.first == "room_type" }?.second, + identityServer = identityServerHost, + token = token, + privateKey = privateKey + ) + } catch (failure: Throwable) { + Timber.i("## Permalink: Failed to parse permalink $signUrl") + PermalinkData.FallbackLink(uri) + } + } else { + PermalinkData.RoomLink( + roomIdOrAlias = identifier, + isRoomAlias = false, + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, + viaParameters = viaQueryParameters.toImmutableList() + ) + } + } + + private fun safeExtractParams(fragment: String) = + fragment.substringAfter("?").split('&').mapNotNull { + val splitNameValue = it.split("=") + if (splitNameValue.size == 2) { + Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8")) + } else { + null + } + } + + private fun String.getViaParameters(): List { + return runCatching { + UrlQuerySanitizer(this) + .parameterList + .filter { + it.mParameter == "via" + } + .map { + URLDecoder.decode(it.mValue, "UTF-8") + } + }.getOrDefault(emptyList()) + } +} 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 622d39bd61..0531c59d4a 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 @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.impl.room +import io.element.android.appconfig.MatrixConfiguration import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.matrix.api.core.EventId @@ -70,8 +71,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.EventTimelineItem @@ -163,7 +169,11 @@ class RustMatrixRoom( override val syncUpdateFlow: StateFlow = _syncUpdateFlow.asStateFlow() init { - timeline.membershipChangeEventReceived + val powerLevelChanges = roomInfoFlow.map { it.userPowerLevels }.distinctUntilChanged() + val membershipChanges = timeline.membershipChangeEventReceived.onStart { emit(Unit) } + combine(membershipChanges, powerLevelChanges) { _, _ -> } + // Skip initial one + .drop(1) // The new events should already be in the SDK cache, no need to fetch them from the server .onEach { roomMemberListFetcher.fetchRoomMembers(source = RoomMemberListFetcher.Source.CACHE) } .launchIn(roomCoroutineScope) @@ -182,45 +192,40 @@ class RustMatrixRoom( } override val name: String? - get() { - return roomListItem.name() - } + get() = runCatching { roomListItem.name() }.getOrDefault(null) override val displayName: String - get() { - return innerRoom.displayName() - } + get() = runCatching { innerRoom.displayName() }.getOrDefault("") override val topic: String? - get() { - return innerRoom.topic() - } + get() = runCatching { innerRoom.topic() }.getOrDefault(null) override val avatarUrl: String? - get() { - return roomListItem.avatarUrl() ?: innerRoom.avatarUrl() - } + get() = runCatching { roomListItem.avatarUrl() ?: innerRoom.avatarUrl() }.getOrDefault(null) override val isEncrypted: Boolean get() = runCatching { innerRoom.isEncrypted() }.getOrDefault(false) override val alias: String? - get() = innerRoom.canonicalAlias() + get() = runCatching { innerRoom.canonicalAlias() }.getOrDefault(null) override val alternativeAliases: List - get() = innerRoom.alternativeAliases() + get() = runCatching { innerRoom.alternativeAliases() }.getOrDefault(emptyList()) override val isPublic: Boolean - get() = innerRoom.isPublic() + get() = runCatching { innerRoom.isPublic() }.getOrDefault(false) + + override val isSpace: Boolean + get() = runCatching { innerRoom.isSpace() }.getOrDefault(false) override val isDirect: Boolean - get() = innerRoom.isDirect() + get() = runCatching { innerRoom.isDirect() }.getOrDefault(false) override val joinedMemberCount: Long - get() = innerRoom.joinedMembersCount().toLong() + get() = runCatching { innerRoom.joinedMembersCount().toLong() }.getOrDefault(0) override val activeMemberCount: Long - get() = innerRoom.activeMembersCount().toLong() + get() = runCatching { innerRoom.activeMembersCount().toLong() }.getOrDefault(0) override suspend fun updateMembers() { val useCache = membersStateFlow.value is MatrixRoomMembersState.Unknown @@ -232,6 +237,16 @@ class RustMatrixRoom( roomMemberListFetcher.fetchRoomMembers(source = source) } + override suspend fun getMembers(limit: Int) = withContext(roomDispatcher) { + runCatching { + innerRoom.members().use { + it.nextChunk(limit.toUInt()).orEmpty().map { roomMember -> + RoomMemberMapper.map(roomMember) + } + } + } + } + override suspend fun getUpdatedMember(userId: UserId): Result = withContext(roomDispatcher) { runCatching { RoomMemberMapper.map(innerRoom.member(userId.value)) @@ -716,6 +731,19 @@ class RustMatrixRoom( ) } + override suspend fun getPermalinkFor(eventId: EventId): Result { + // FIXME Use the SDK API once https://github.com/matrix-org/matrix-rust-sdk/issues/3259 has been done + // Now use a simple builder + return runCatching { + buildString { + append(MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL) + append(roomId.value) + append("/") + append(eventId.value) + } + } + } + private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt new file mode 100644 index 0000000000..876a58d3a5 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDescriptionMapper.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.roomdirectory + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription +import org.matrix.rustcomponents.sdk.PublicRoomJoinRule +import org.matrix.rustcomponents.sdk.RoomDescription as RustRoomDescription + +class RoomDescriptionMapper { + fun map(roomDescription: RustRoomDescription): RoomDescription { + return RoomDescription( + roomId = RoomId(roomDescription.roomId), + name = roomDescription.name, + topic = roomDescription.topic, + avatarUrl = roomDescription.avatarUrl, + alias = roomDescription.alias, + joinRule = when (roomDescription.joinRule) { + PublicRoomJoinRule.PUBLIC -> RoomDescription.JoinRule.PUBLIC + PublicRoomJoinRule.KNOCK -> RoomDescription.JoinRule.KNOCK + null -> RoomDescription.JoinRule.UNKNOWN + }, + isWorldReadable = roomDescription.isWorldReadable, + numberOfMembers = roomDescription.joinedMembers.toLong(), + ) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchExtension.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchExtension.kt new file mode 100644 index 0000000000..28e82f4e40 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchExtension.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.roomdirectory + +import io.element.android.libraries.matrix.impl.util.cancelAndDestroy +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import org.matrix.rustcomponents.sdk.RoomDirectorySearch +import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntriesListener +import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate +import timber.log.Timber + +internal fun RoomDirectorySearch.resultsFlow(): Flow> = + callbackFlow { + val listener = object : RoomDirectorySearchEntriesListener { + override fun onUpdate(roomEntriesUpdate: List) { + trySendBlocking(roomEntriesUpdate) + } + } + val result = results(listener) + awaitClose { + result.cancelAndDestroy() + } + }.catch { + Timber.d(it, "timelineDiffFlow() failed") + }.buffer(Channel.UNLIMITED) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt new file mode 100644 index 0000000000..aff631d6b4 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RoomDirectorySearchProcessor.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.roomdirectory + +import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate +import timber.log.Timber +import kotlin.coroutines.CoroutineContext + +class RoomDirectorySearchProcessor( + private val roomDescriptions: MutableSharedFlow>, + private val coroutineContext: CoroutineContext, + private val roomDescriptionMapper: RoomDescriptionMapper, +) { + private val mutex = Mutex() + + suspend fun postUpdates(updates: List) { + updateRoomDescriptions { + Timber.v("Update room descriptions from postUpdates (with ${updates.size} items) on ${Thread.currentThread()}") + updates.forEach { update -> + applyUpdate(update) + } + } + } + + private fun MutableList.applyUpdate(update: RoomDirectorySearchEntryUpdate) { + when (update) { + is RoomDirectorySearchEntryUpdate.Append -> { + val roomSummaries = update.values.map(roomDescriptionMapper::map) + addAll(roomSummaries) + } + is RoomDirectorySearchEntryUpdate.PushBack -> { + val roomDescription = roomDescriptionMapper.map(update.value) + add(roomDescription) + } + is RoomDirectorySearchEntryUpdate.PushFront -> { + val roomDescription = roomDescriptionMapper.map(update.value) + add(0, roomDescription) + } + is RoomDirectorySearchEntryUpdate.Set -> { + val roomDescription = roomDescriptionMapper.map(update.value) + this[update.index.toInt()] = roomDescription + } + is RoomDirectorySearchEntryUpdate.Insert -> { + val roomDescription = roomDescriptionMapper.map(update.value) + add(update.index.toInt(), roomDescription) + } + is RoomDirectorySearchEntryUpdate.Remove -> { + removeAt(update.index.toInt()) + } + is RoomDirectorySearchEntryUpdate.Reset -> { + clear() + addAll(update.values.map(roomDescriptionMapper::map)) + } + RoomDirectorySearchEntryUpdate.PopBack -> { + removeLastOrNull() + } + RoomDirectorySearchEntryUpdate.PopFront -> { + removeFirstOrNull() + } + RoomDirectorySearchEntryUpdate.Clear -> { + clear() + } + is RoomDirectorySearchEntryUpdate.Truncate -> { + subList(update.length.toInt(), size).clear() + } + } + } + + private suspend fun updateRoomDescriptions(block: suspend MutableList.() -> Unit) = withContext(coroutineContext) { + mutex.withLock { + val current = roomDescriptions.replayCache.lastOrNull() + val mutableRoomSummaries = current.orEmpty().toMutableList() + block(mutableRoomSummaries) + roomDescriptions.emit(mutableRoomSummaries) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt new file mode 100644 index 0000000000..9b444d5ce5 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryList.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.roomdirectory + +import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.matrix.rustcomponents.sdk.RoomDirectorySearch +import kotlin.coroutines.CoroutineContext + +class RustRoomDirectoryList( + private val inner: RoomDirectorySearch, + coroutineScope: CoroutineScope, + private val coroutineContext: CoroutineContext, +) : RoomDirectoryList { + private val hasMoreToLoad = MutableStateFlow(true) + private val items = MutableSharedFlow>(replay = 1) + private val processor = RoomDirectorySearchProcessor(items, coroutineContext, RoomDescriptionMapper()) + + init { + launchIn(coroutineScope) + } + + private fun launchIn(coroutineScope: CoroutineScope) { + inner + .resultsFlow() + .onEach { updates -> + processor.postUpdates(updates) + } + .flowOn(coroutineContext) + .launchIn(coroutineScope) + } + + override suspend fun filter(filter: String?, batchSize: Int): Result { + return execute { + inner.search(filter = filter, batchSize = batchSize.toUInt()) + } + } + + override suspend fun loadMore(): Result { + return execute { + inner.nextPage() + } + } + + private suspend fun execute(action: suspend () -> Unit): Result { + return try { + // We always assume there is more to load until we know there isn't. + // As accessing hasMoreToLoad is otherwise blocked by the current action. + hasMoreToLoad.value = true + action() + Result.success(Unit) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } finally { + hasMoreToLoad.value = hasMoreToLoad() + } + } + + private suspend fun hasMoreToLoad(): Boolean { + return !inner.isAtLastPage() + } + + override val state: Flow = + combine(hasMoreToLoad, items) { hasMoreToLoad, items -> + RoomDirectoryList.State( + hasMoreToLoad = hasMoreToLoad, + items = items + ) + } + .flowOn(coroutineContext) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt new file mode 100644 index 0000000000..2939001b21 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryService.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.roomdirectory + +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import org.matrix.rustcomponents.sdk.Client + +class RustRoomDirectoryService( + private val client: Client, + private val sessionDispatcher: CoroutineDispatcher, +) : RoomDirectoryService { + override fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList { + return RustRoomDirectoryList(client.roomDirectorySearch(), scope, sessionDispatcher) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index 157e751d21..3d2fe0cc40 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -18,90 +18,123 @@ package io.element.android.libraries.matrix.impl.verification import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState -import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.util.cancelAndDestroy +import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Encryption import org.matrix.rustcomponents.sdk.RecoveryState import org.matrix.rustcomponents.sdk.RecoveryStateListener import org.matrix.rustcomponents.sdk.SessionVerificationController import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate -import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface -import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.VerificationState +import org.matrix.rustcomponents.sdk.VerificationStateListener import org.matrix.rustcomponents.sdk.use +import timber.log.Timber +import kotlin.time.Duration.Companion.seconds import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData class RustSessionVerificationService( - client: Client, - private val syncService: RustSyncService, + private val client: Client, + isSyncServiceReady: Flow, private val sessionCoroutineScope: CoroutineScope, + private val sessionStore: SessionStore, ) : SessionVerificationService, SessionVerificationControllerDelegate { - private var recoveryStateListenerTaskHandle: TaskHandle? = null private val encryptionService: Encryption = client.encryption() - var verificationController: SessionVerificationControllerInterface? = null - set(value) { - field = value - _isReady.value = value != null - // If status was 'Unknown', move it to either 'Verified' or 'NotVerified' - if (value != null) { - value.setDelegate(this) - sessionCoroutineScope.launch { updateVerificationStatus(value.isVerified()) } - } + private lateinit var verificationController: SessionVerificationController + + // Listen for changes in verification status and update accordingly + private val verificationStateListenerTaskHandle = encryptionService.verificationStateListener(object : VerificationStateListener { + override fun onUpdate(status: VerificationState) { + Timber.d("New verification state: $status") + updateVerificationStatus(status) } + }) + + // In case we enter the recovery key instead we check changes in the recovery state, since the listener above won't be triggered + private val recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener { + override fun onUpdate(status: RecoveryState) { + Timber.d("New recovery state: $status") + // We could check the `RecoveryState`, but it's easier to just use the verification state directly + updateVerificationStatus(encryptionService.verificationState()) + } + }) + + override val needsVerificationFlow: StateFlow = sessionStore.sessionsFlow() + .map { sessions -> sessions.firstOrNull { it.userId == client.userId() }?.needsVerification.orFalse() } + .distinctUntilChanged() + .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) private val _verificationFlowState = MutableStateFlow(VerificationFlowState.Initial) override val verificationFlowState = _verificationFlowState.asStateFlow() - private val _isReady = MutableStateFlow(false) - override val isReady = _isReady.asStateFlow() - private val _sessionVerifiedStatus = MutableStateFlow(SessionVerifiedStatus.Unknown) override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus.asStateFlow() - override val canVerifySessionFlow = combine(sessionVerifiedStatus, syncService.syncState) { verificationStatus, syncState -> - syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified + override val isReady = isSyncServiceReady.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) + + override val canVerifySessionFlow = combine(sessionVerifiedStatus, isReady) { verificationStatus, isReady -> + isReady && verificationStatus == SessionVerifiedStatus.NotVerified } - fun start() { - recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener { - override fun onUpdate(status: RecoveryState) { - sessionCoroutineScope.launch { - updateVerificationStatus(verificationController?.isVerified().orFalse()) - } + init { + isReady.onEach { isReady -> + if (isReady) { + Timber.d("Starting verification service") + // Immediate status update + updateVerificationStatus(encryptionService.verificationState()) + } else { + Timber.d("Stopping verification service") } - }) + } + .launchIn(sessionCoroutineScope) } override suspend fun requestVerification() = tryOrFail { - verificationController?.requestVerification() + if (!this::verificationController.isInitialized) { + verificationController = client.getSessionVerificationController() + verificationController.setDelegate(this) + } + verificationController.requestVerification() } - override suspend fun cancelVerification() = tryOrFail { verificationController?.cancelVerification() } + override suspend fun cancelVerification() = tryOrFail { verificationController.cancelVerification() } - override suspend fun approveVerification() = tryOrFail { verificationController?.approveVerification() } + override suspend fun approveVerification() = tryOrFail { verificationController.approveVerification() } - override suspend fun declineVerification() = tryOrFail { verificationController?.declineVerification() } + override suspend fun declineVerification() = tryOrFail { verificationController.declineVerification() } override suspend fun startVerification() = tryOrFail { - verificationController?.startSasVerification() + verificationController.startSasVerification() } private suspend fun tryOrFail(block: suspend () -> Unit) { runCatching { block() - }.onFailure { didFail() } + }.onFailure { + Timber.e(it, "Failed to verify session") + didFail() + } } // region Delegate implementation @@ -116,13 +149,31 @@ class RustSessionVerificationService( } override fun didFail() { + Timber.e("Session verification failed with an unknown error") _verificationFlowState.value = VerificationFlowState.Failed } override fun didFinish() { - _verificationFlowState.value = VerificationFlowState.Finished - // Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it always returns false - updateVerificationStatus(isVerified = true) + sessionCoroutineScope.launch { + // Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it returns false if run immediately + // It also sometimes unexpectedly fails to report the session as verified, so we have to handle that possibility and fail if needed + runCatching { + withTimeout(30.seconds) { + while (!verificationController.isVerified()) { + delay(100) + } + } + } + .onSuccess { + saveVerifiedState(true) + updateVerificationStatus(VerificationState.VERIFIED) + _verificationFlowState.value = VerificationFlowState.Finished + } + .onFailure { + Timber.e(it, "Verification finished, but the Rust SDK still reports the session as unverified.") + didFail() + } + } } override fun didReceiveVerificationData(data: RustSessionVerificationData) { @@ -139,25 +190,35 @@ class RustSessionVerificationService( override suspend fun reset() { if (isReady.value) { // Cancel any pending verification attempt - tryOrNull { verificationController?.cancelVerification() } + tryOrNull { verificationController.cancelVerification() } } _verificationFlowState.value = VerificationFlowState.Initial } - fun destroy() { - recoveryStateListenerTaskHandle?.cancelAndDestroy() - verificationController?.setDelegate(null) - (verificationController as? SessionVerificationController)?.destroy() - verificationController = null + override suspend fun saveVerifiedState(verified: Boolean) = tryOrFail { + val existingSession = sessionStore.getSession(client.userId()) + ?: error("Failed to save verification state. No session with id ${client.userId()}") + sessionStore.updateData(existingSession.copy(needsVerification = !verified)) + // Wait until the new state is saved + needsVerificationFlow.first { needsVerification -> !needsVerification } } - private fun updateVerificationStatus(isVerified: Boolean) { - val newValue = when { - !isReady.value -> SessionVerifiedStatus.Unknown - !isVerified -> SessionVerifiedStatus.NotVerified - else -> SessionVerifiedStatus.Verified + fun destroy() { + Timber.d("Destroying RustSessionVerificationService") + verificationStateListenerTaskHandle.cancelAndDestroy() + recoveryStateListenerTaskHandle.cancelAndDestroy() + if (this::verificationController.isInitialized) { + verificationController.setDelegate(null) + verificationController.destroy() + } + } + + private fun updateVerificationStatus(verificationState: VerificationState) { + _sessionVerifiedStatus.value = when (verificationState) { + VerificationState.UNKNOWN -> SessionVerifiedStatus.Unknown + VerificationState.VERIFIED -> SessionVerifiedStatus.Verified + VerificationState.UNVERIFIED -> SessionVerifiedStatus.NotVerified } - _sessionVerifiedStatus.value = newValue } } diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt similarity index 71% rename from libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt index cbf1bffc88..7401ac7c2e 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.api.permalink +package io.element.android.libraries.matrix.impl.permalink import android.net.Uri import com.google.common.truth.Truth.assertThat @@ -23,34 +23,34 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class MatrixToConverterTest { +class DefaultMatrixToConverterTest { @Test fun `converting a matrix-to url does nothing`() { val url = Uri.parse("https://matrix.to/#/#element-android:matrix.org") - assertThat(MatrixToConverter.convert(url)).isEqualTo(url) + assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(url) } @Test fun `converting a url with a supported room path returns a matrix-to url`() { val url = Uri.parse("https://riot.im/develop/#/room/#element-android:matrix.org") - assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/#element-android:matrix.org")) + assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/#element-android:matrix.org")) } @Test fun `converting a url with a supported user path returns a matrix-to url`() { val url = Uri.parse("https://riot.im/develop/#/user/@test:matrix.org") - assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/@test:matrix.org")) + assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/@test:matrix.org")) } @Test fun `converting a url with a supported group path returns a matrix-to url`() { val url = Uri.parse("https://riot.im/develop/#/group/+group:matrix.org") - assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/+group:matrix.org")) + assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/+group:matrix.org")) } @Test fun `converting an unsupported url returns null`() { val url = Uri.parse("https://element.io/") - assertThat(MatrixToConverter.convert(url)).isNull() + assertThat(DefaultMatrixToConverter().convert(url)).isNull() } } diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilderTest.kt similarity index 70% rename from libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilderTest.kt index 2b5b29e084..c861b3105c 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilderTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.api.permalink +package io.element.android.libraries.matrix.impl.permalink import com.google.common.truth.Truth.assertThat import io.element.android.libraries.androidutils.metadata.withReleaseBehavior @@ -23,18 +23,18 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.tests.testutils.assertThrowsInDebug import org.junit.Test -class PermalinkBuilderTest { +class DefaultPermalinkBuilderTest { fun `building a permalink for an invalid user id throws when verifying the id`() { assertThrowsInDebug { val userId = UserId("some invalid user id") - PermalinkBuilder.permalinkForUser(userId) + DefaultPermalinkBuilder().permalinkForUser(userId) } } fun `building a permalink for an invalid room id throws when verifying the id`() { assertThrowsInDebug { val roomId = RoomId("some invalid room id") - PermalinkBuilder.permalinkForRoomId(roomId) + DefaultPermalinkBuilder().permalinkForRoomId(roomId) } } @@ -42,7 +42,7 @@ class PermalinkBuilderTest { fun `building a permalink for an invalid user id returns failure when not verifying the id`() { withReleaseBehavior { val userId = UserId("some invalid user id") - assertThat(PermalinkBuilder.permalinkForUser(userId).isFailure).isTrue() + assertThat(DefaultPermalinkBuilder().permalinkForUser(userId).isFailure).isTrue() } } @@ -50,31 +50,31 @@ class PermalinkBuilderTest { fun `building a permalink for an invalid room id returns failure when not verifying the id`() { withReleaseBehavior { val roomId = RoomId("some invalid room id") - assertThat(PermalinkBuilder.permalinkForRoomId(roomId).isFailure).isTrue() + assertThat(DefaultPermalinkBuilder().permalinkForRoomId(roomId).isFailure).isTrue() } } @Test fun `building a permalink for an invalid room alias returns failure`() { val roomAlias = "an invalid room alias" - assertThat(PermalinkBuilder.permalinkForRoomAlias(roomAlias).isFailure).isTrue() + assertThat(DefaultPermalinkBuilder().permalinkForRoomAlias(roomAlias).isFailure).isTrue() } @Test fun `building a permalink for a valid user id returns a matrix-to url`() { val userId = UserId("@user:matrix.org") - assertThat(PermalinkBuilder.permalinkForUser(userId).getOrNull()).isEqualTo("https://matrix.to/#/@user:matrix.org") + assertThat(DefaultPermalinkBuilder().permalinkForUser(userId).getOrNull()).isEqualTo("https://matrix.to/#/@user:matrix.org") } @Test fun `building a permalink for a valid room id returns a matrix-to url`() { val roomId = RoomId("!aBCdEFG1234:matrix.org") - assertThat(PermalinkBuilder.permalinkForRoomId(roomId).getOrNull()).isEqualTo("https://matrix.to/#/!aBCdEFG1234:matrix.org") + assertThat(DefaultPermalinkBuilder().permalinkForRoomId(roomId).getOrNull()).isEqualTo("https://matrix.to/#/!aBCdEFG1234:matrix.org") } @Test fun `building a permalink for a valid room alias returns a matrix-to url`() { val roomAlias = "#room:matrix.org" - assertThat(PermalinkBuilder.permalinkForRoomAlias(roomAlias).getOrNull()).isEqualTo("https://matrix.to/#/#room:matrix.org") + assertThat(DefaultPermalinkBuilder().permalinkForRoomAlias(roomAlias).getOrNull()).isEqualTo("https://matrix.to/#/#room:matrix.org") } } diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParserTest.kt similarity index 70% rename from libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParserTest.kt index 66cdaf88ee..1e9e3bc2dc 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParserTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,44 +14,60 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.api.permalink +package io.element.android.libraries.matrix.impl.permalink import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.permalink.PermalinkData import kotlinx.collections.immutable.persistentListOf import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class PermalinkParserTest { +class DefaultPermalinkParserTest { @Test fun `parsing an invalid url returns a fallback link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://element.io" - assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) } @Test fun `parsing an invalid url with the right path but no content returns a fallback link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/user" - assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) } @Test fun `parsing an invalid url with the right path but empty content returns a fallback link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/user/" - assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) } @Test fun `parsing an invalid url with the right path but invalid content returns a fallback link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/user/some%20user!" - assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) } @Test fun `parsing a valid user url returns a user link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/user/@test:matrix.org" - assertThat(PermalinkParser.parse(url)).isEqualTo( + assertThat(sut.parse(url)).isEqualTo( PermalinkData.UserLink( userId = "@test:matrix.org" ) @@ -60,8 +76,11 @@ class PermalinkParserTest { @Test fun `parsing a valid room id url returns a room link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/room/!aBCD1234:matrix.org" - assertThat(PermalinkParser.parse(url)).isEqualTo( + assertThat(sut.parse(url)).isEqualTo( PermalinkData.RoomLink( roomIdOrAlias = "!aBCD1234:matrix.org", isRoomAlias = false, @@ -73,8 +92,11 @@ class PermalinkParserTest { @Test fun `parsing a valid room id with event id url returns a room link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/$1234567890abcdef:matrix.org" - assertThat(PermalinkParser.parse(url)).isEqualTo( + assertThat(sut.parse(url)).isEqualTo( PermalinkData.RoomLink( roomIdOrAlias = "!aBCD1234:matrix.org", isRoomAlias = false, @@ -86,8 +108,11 @@ class PermalinkParserTest { @Test fun `parsing a valid room id with and invalid event id url returns a room link with no event id`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/1234567890abcdef:matrix.org" - assertThat(PermalinkParser.parse(url)).isEqualTo( + assertThat(sut.parse(url)).isEqualTo( PermalinkData.RoomLink( roomIdOrAlias = "!aBCD1234:matrix.org", isRoomAlias = false, @@ -99,8 +124,11 @@ class PermalinkParserTest { @Test fun `parsing a valid room id with event id and via parameters url returns a room link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/$1234567890abcdef:matrix.org?via=matrix.org&via=matrix.com" - assertThat(PermalinkParser.parse(url)).isEqualTo( + assertThat(sut.parse(url)).isEqualTo( PermalinkData.RoomLink( roomIdOrAlias = "!aBCD1234:matrix.org", isRoomAlias = false, @@ -112,8 +140,11 @@ class PermalinkParserTest { @Test fun `parsing a valid room alias url returns a room link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/room/#element-android:matrix.org" - assertThat(PermalinkParser.parse(url)).isEqualTo( + assertThat(sut.parse(url)).isEqualTo( PermalinkData.RoomLink( roomIdOrAlias = "#element-android:matrix.org", isRoomAlias = true, @@ -125,6 +156,9 @@ class PermalinkParserTest { @Test fun `parsing a url with an invalid signurl returns a fallback link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) // This url has no private key val url = "https://app.element.io/#/room/%21aBCDEF12345%3Amatrix.org" + "?email=testuser%40element.io" + @@ -135,11 +169,14 @@ class PermalinkParserTest { "&guest_access_token=" + "&guest_user_id=" + "&room_type=" - assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) + assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java) } @Test fun `parsing a url with signurl returns a room email invite link`() { + val sut = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) val url = "https://app.element.io/#/room/%21aBCDEF12345%3Amatrix.org" + "?email=testuser%40element.io" + "&signurl=https%3A%2F%2Fvector.im%2F_matrix%2Fidentity%2Fapi%2Fv1%2Fsign-ed25519%3Ftoken%3Da_token%26private_key%3Da_private_key" + @@ -149,7 +186,7 @@ class PermalinkParserTest { "&guest_access_token=" + "&guest_user_id=" + "&room_type=" - assertThat(PermalinkParser.parse(url)).isEqualTo( + assertThat(sut.parse(url)).isEqualTo( PermalinkData.RoomEmailInviteLink( roomId = "!aBCDEF12345:matrix.org", email = "testuser@element.io", 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 d7c3f71e64..b485257815 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 @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser @@ -39,6 +40,7 @@ import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.pushers.FakePushersService +import io.element.android.libraries.matrix.test.roomdirectory.FakeRoomDirectoryService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService @@ -65,6 +67,7 @@ class FakeMatrixClient( private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val syncService: FakeSyncService = FakeSyncService(), private val encryptionService: FakeEncryptionService = FakeEncryptionService(), + private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), private val accountManagementUrlString: Result = Result.success(null), ) : MatrixClient { var setDisplayNameCalled: Boolean = false @@ -91,6 +94,9 @@ class FakeMatrixClient( private var setDisplayNameResult: Result = Result.success(Unit) private var uploadAvatarResult: Result = Result.success(Unit) private var removeAvatarResult: Result = Result.success(Unit) + var joinRoomLambda: suspend (RoomId) -> Result = { + Result.success(it) + } override suspend fun getRoom(roomId: RoomId): MatrixRoom? { return getRoomResults[roomId] @@ -126,6 +132,8 @@ class FakeMatrixClient( override fun syncService() = syncService + override fun roomDirectoryService() = roomDirectoryService + override suspend fun getCacheSize(): Long { return 0 } @@ -176,6 +184,8 @@ class FakeMatrixClient( return removeAvatarResult } + override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) + override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun pushersService(): PushersService = pushersService @@ -245,4 +255,16 @@ class FakeMatrixClient( fun givenRemoveAvatarResult(result: Result) { removeAvatarResult = result } + + private val visitedRoomsId: MutableList = mutableListOf() + + override suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result { + visitedRoomsId.removeAll { it == roomId } + visitedRoomsId.add(0, roomId) + return Result.success(Unit) + } + + override suspend fun getRecentlyVisitedRooms(): Result> { + return Result.success(visitedRoomsId) + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkBuilder.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkBuilder.kt new file mode 100644 index 0000000000..4e6d3375e1 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkBuilder.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.test.permalink + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder + +class FakePermalinkBuilder( + private val result: () -> Result = { Result.failure(Exception("Not implemented")) } +) : PermalinkBuilder { + override fun permalinkForUser(userId: UserId): Result { + return result() + } + + override fun permalinkForRoomAlias(roomAlias: String): Result { + return result() + } + + override fun permalinkForRoomId(roomId: RoomId): Result { + return result() + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt new file mode 100644 index 0000000000..f046eadf93 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.test.permalink + +import android.net.Uri +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser + +class FakePermalinkParser( + private var result: () -> PermalinkData = { TODO("Not implemented") } +) : PermalinkParser { + fun givenResult(result: PermalinkData) { + this.result = { result } + } + + override fun parse(uriString: String): PermalinkData { + return result() + } + + override fun parse(uri: Uri): PermalinkData { + TODO("Not yet implemented") + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 1872d2e60d..11582433d5 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -77,12 +77,14 @@ class FakeMatrixRoom( override val alias: String? = null, override val alternativeAliases: List = emptyList(), override val isPublic: Boolean = true, + override val isSpace: Boolean = false, override val isDirect: Boolean = false, override val isOneToOne: Boolean = false, override val joinedMemberCount: Long = 123L, override val activeMemberCount: Long = 234L, val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), + private var permalinkResult: () -> Result = { Result.success("link") }, canRedactOwn: Boolean = false, canRedactOther: Boolean = false, ) : MatrixRoom { @@ -199,6 +201,10 @@ class FakeMatrixRoom( return getRoomMemberResult } + override suspend fun getMembers(limit: Int): Result> { + return Result.success(emptyList()) + } + override suspend fun updateRoomNotificationSettings(): Result = simulateLongTask { val notificationSettings = notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() roomNotificationSettingsStateFlow.value = MatrixRoomNotificationSettingsState.Ready(notificationSettings) @@ -272,6 +278,10 @@ class FakeMatrixRoom( return cancelSendResult } + override suspend fun getPermalinkFor(eventId: EventId): Result { + return permalinkResult() + } + override suspend fun editMessage( originalEventId: EventId?, transactionId: TransactionId?, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt new file mode 100644 index 0000000000..b01501d328 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryList.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.test.roomdirectory + +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +class FakeRoomDirectoryList( + override val state: Flow = emptyFlow(), + val filterLambda: (String?, Int) -> Result = { _, _ -> Result.success(Unit) }, + val loadMoreLambda: () -> Result = { Result.success(Unit) } +) : RoomDirectoryList { + override suspend fun filter(filter: String?, batchSize: Int) = filterLambda(filter, batchSize) + + override suspend fun loadMore(): Result = loadMoreLambda() +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt new file mode 100644 index 0000000000..68926b9deb --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/FakeRoomDirectoryService.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.test.roomdirectory + +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList +import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService +import kotlinx.coroutines.CoroutineScope + +class FakeRoomDirectoryService( + private val createRoomDirectoryListFactory: (CoroutineScope) -> RoomDirectoryList = { throw AssertionError("Configure a proper factory.") } +) : RoomDirectoryService { + override fun createRoomDirectoryList(scope: CoroutineScope) = createRoomDirectoryListFactory(scope) +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt new file mode 100644 index 0000000000..6e96ca4452 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomdirectory/RoomDescriptionFixture.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.test.roomdirectory + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription +import io.element.android.libraries.matrix.test.A_ROOM_ID + +fun aRoomDescription( + roomId: RoomId = A_ROOM_ID, + name: String? = null, + topic: String? = null, + alias: String? = null, + avatarUrl: String? = null, + joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, + isWorldReadable: Boolean = true, + joinedMembers: Long = 2L +) = RoomDescription( + roomId = roomId, + name = name, + topic = topic, + alias = alias, + avatarUrl = avatarUrl, + joinRule = joinRule, + isWorldReadable = isWorldReadable, + numberOfMembers = joinedMembers +) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt index 4a7aa1c304..7823910263 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt @@ -20,17 +20,22 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder +import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class FakeSessionVerificationService : SessionVerificationService { +class FakeSessionVerificationService( + var saveVerifiedStateResult: LambdaOneParamRecorder = lambdaRecorder {} +) : SessionVerificationService { private val _isReady = MutableStateFlow(false) private val _sessionVerifiedStatus = MutableStateFlow(SessionVerifiedStatus.Unknown) private var _verificationFlowState = MutableStateFlow(VerificationFlowState.Initial) private var _canVerifySessionFlow = MutableStateFlow(true) var shouldFail = false + override val needsVerificationFlow: MutableStateFlow = MutableStateFlow(false) override val verificationFlowState: StateFlow = _verificationFlowState override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus override val canVerifySessionFlow: Flow = _canVerifySessionFlow @@ -38,7 +43,11 @@ class FakeSessionVerificationService : SessionVerificationService { override val isReady: StateFlow = _isReady override suspend fun requestVerification() { - _verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest + if (!shouldFail) { + _verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest + } else { + _verificationFlowState.value = VerificationFlowState.Failed + } } override suspend fun cancelVerification() { @@ -85,7 +94,15 @@ class FakeSessionVerificationService : SessionVerificationService { _isReady.value = value } + fun givenNeedsVerification(value: Boolean) { + needsVerificationFlow.value = value + } + override suspend fun reset() { _verificationFlowState.value = VerificationFlowState.Initial } + + override suspend fun saveVerifiedState(verified: Boolean) { + saveVerifiedStateResult(verified) + } } diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index 669f9c9634..fa7a946959 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -47,4 +47,5 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) testImplementation(libs.test.robolectric) + testImplementation(projects.libraries.matrix.test) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt index b3c6a0bf86..36ae94d7f7 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt @@ -29,9 +29,13 @@ import org.jsoup.nodes.Document * * This will also make sure mentions are prefixed with `@`. * + * @param permalinkParser the parser to use to parse the mentions. * @param prefix if not null, the prefix will be inserted at the beginning of the message. */ -fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? { +fun FormattedBody.toHtmlDocument( + permalinkParser: PermalinkParser, + prefix: String? = null, +): Document? { return takeIf { it.format == MessageFormat.HTML }?.body // Trim whitespace at the end to avoid having wrong rendering of the message. // We don't trim the start in case it's used as indentation. @@ -44,17 +48,20 @@ fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? { } // Prepend `@` to mentions - fixMentions(dom) + fixMentions(dom, permalinkParser) dom } } -private fun fixMentions(dom: Document) { +private fun fixMentions( + dom: Document, + permalinkParser: PermalinkParser, +) { val links = dom.getElementsByTag("a") links.forEach { if (it.hasAttr("href")) { - val link = PermalinkParser.parse(it.attr("href")) + val link = permalinkParser.parse(it.attr("href")) if (link is PermalinkData.UserLink && !it.text().startsWith("@")) { it.prependText("@") } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt index 0cea2d7ae2..1eb581af65 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.ui.messages +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType @@ -29,15 +30,24 @@ import org.jsoup.select.NodeVisitor * Converts the HTML string in [TextMessageType.formatted] to a plain text representation by parsing it and removing all formatting. * If the message is not formatted or the format is not [MessageFormat.HTML], the [TextMessageType.body] is returned instead. */ -fun TextMessageType.toPlainText() = formatted?.toPlainText() ?: body +fun TextMessageType.toPlainText( + permalinkParser: PermalinkParser, +) = formatted?.toPlainText(permalinkParser) ?: body /** * Converts the HTML string in [FormattedBody.body] to a plain text representation by parsing it and removing all formatting. * If the message is not formatted or the format is not [MessageFormat.HTML] we return `null`. + * @param permalinkParser the parser to use to parse the mentions. * @param prefix if not null, the prefix will be inserted at the beginning of the message. */ -fun FormattedBody.toPlainText(prefix: String? = null): String? { - return this.toHtmlDocument(prefix)?.toPlainText() +fun FormattedBody.toPlainText( + permalinkParser: PermalinkParser, + prefix: String? = null, +): String? { + return this.toHtmlDocument( + permalinkParser = permalinkParser, + prefix = prefix, + )?.toPlainText() } /** diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt index 704b87c593..ab83f77e66 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt @@ -16,9 +16,13 @@ package io.element.android.libraries.matrixui.messages +import android.net.Uri import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.ui.messages.toHtmlDocument import org.junit.Test import org.junit.runner.RunWith @@ -33,7 +37,7 @@ class ToHtmlDocumentTest { body = "Hello world" ) - val document = body.toHtmlDocument() + val document = body.toHtmlDocument(permalinkParser = FakePermalinkParser()) assertThat(document).isNull() } @@ -45,7 +49,7 @@ class ToHtmlDocumentTest { body = "

Hello world

" ) - val document = body.toHtmlDocument() + val document = body.toHtmlDocument(permalinkParser = FakePermalinkParser()) assertThat(document).isNotNull() assertThat(document?.text()).isEqualTo("Hello world") } @@ -57,7 +61,10 @@ class ToHtmlDocumentTest { body = "

Hello world

" ) - val document = body.toHtmlDocument(prefix = "@Jorge:") + val document = body.toHtmlDocument( + permalinkParser = FakePermalinkParser(), + prefix = "@Jorge:" + ) assertThat(document).isNotNull() assertThat(document?.text()).isEqualTo("@Jorge: Hello world") } @@ -69,7 +76,13 @@ class ToHtmlDocumentTest { body = "Hey Alice!" ) - val document = body.toHtmlDocument() + val document = body.toHtmlDocument(permalinkParser = object : PermalinkParser { + override fun parse(uriString: String): PermalinkData { + return PermalinkData.UserLink("@alice:matrix.org") + } + + override fun parse(uri: Uri): PermalinkData = TODO("Not yet implemented") + }) assertThat(document?.text()).isEqualTo("Hey @Alice!") } @@ -80,7 +93,13 @@ class ToHtmlDocumentTest { body = "Hey @Alice!" ) - val document = body.toHtmlDocument() + val document = body.toHtmlDocument(permalinkParser = object : PermalinkParser { + override fun parse(uriString: String): PermalinkData { + return PermalinkData.UserLink("@alice:matrix.org") + } + + override fun parse(uri: Uri): PermalinkData = TODO("Not yet implemented") + }) assertThat(document?.text()).isEqualTo("Hey @Alice!") } @@ -91,7 +110,13 @@ class ToHtmlDocumentTest { body = "Hey Alice!" ) - val document = body.toHtmlDocument() + val document = body.toHtmlDocument(permalinkParser = object : PermalinkParser { + override fun parse(uriString: String): PermalinkData { + return PermalinkData.FallbackLink(uri = Uri.parse("https://matrix.org")) + } + + override fun parse(uri: Uri): PermalinkData = TODO("Not yet implemented") + }) assertThat(document?.text()).isEqualTo("Hey Alice!") } } diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt index 7c61075584..74e43a9e39 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt @@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.ui.messages.toPlainText import org.jsoup.Jsoup import org.junit.Test @@ -59,7 +60,7 @@ class ToPlainTextTest {
""".trimIndent() ) - assertThat(formattedBody.toPlainText()).isEqualTo( + assertThat(formattedBody.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo( """ Hello world • This is an unordered list. @@ -79,7 +80,7 @@ class ToPlainTextTest {
""".trimIndent() ) - assertThat(formattedBody.toPlainText()).isNull() + assertThat(formattedBody.toPlainText(permalinkParser = FakePermalinkParser())).isNull() } @Test @@ -96,7 +97,7 @@ class ToPlainTextTest { """.trimIndent() ) ) - assertThat(messageType.toPlainText()).isEqualTo( + assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo( """ Hello world • This is an unordered list. @@ -119,6 +120,6 @@ class ToPlainTextTest { """.trimIndent() ) ) - assertThat(messageType.toPlainText()).isEqualTo("This is the fallback text") + assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo("This is the fallback text") } } diff --git a/libraries/permissions/api/build.gradle.kts b/libraries/permissions/api/build.gradle.kts index 32d3776419..cb60c6c0a7 100644 --- a/libraries/permissions/api/build.gradle.kts +++ b/libraries/permissions/api/build.gradle.kts @@ -24,7 +24,6 @@ android { dependencies { implementation(projects.libraries.architecture) - implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) diff --git a/libraries/permissions/api/src/main/res/values-hu/translations.xml b/libraries/permissions/api/src/main/res/values-hu/translations.xml index aea9a1f772..de3573fce2 100644 --- a/libraries/permissions/api/src/main/res/values-hu/translations.xml +++ b/libraries/permissions/api/src/main/res/values-hu/translations.xml @@ -1,7 +1,7 @@ "Hogy az alkalmazás használhassa a kamerát, adja meg az engedélyt a rendszerbeállításokban." - "Add meg az engedélyt a rendszerbeállításokban." + "Adja meg az engedélyt a rendszerbeállításokban." "Hogy az alkalmazás használhassa a mikrofont, adja meg az engedélyt a rendszerbeállításokban." "Hogy az alkalmazás megjeleníthesse az értesítéseket, adja meg az engedélyt a rendszerbeállításokban." diff --git a/libraries/permissions/impl/build.gradle.kts b/libraries/permissions/impl/build.gradle.kts index 7d05d9a1d7..7b80ef0d4f 100644 --- a/libraries/permissions/impl/build.gradle.kts +++ b/libraries/permissions/impl/build.gradle.kts @@ -46,8 +46,10 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) + implementation(projects.libraries.troubleshoot.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) + implementation(projects.services.toolbox.api) api(projects.libraries.permissions.api) testImplementation(libs.test.junit) @@ -57,6 +59,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.permissions.test) + testImplementation(projects.services.toolbox.test) testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt index c05df3de46..61a82aafff 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionStateProvider.kt @@ -17,6 +17,8 @@ package io.element.android.libraries.permissions.impl import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -33,7 +35,10 @@ class DefaultPermissionStateProvider @Inject constructor( private val permissionsStore: PermissionsStore, ) : PermissionStateProvider { override fun isPermissionGranted(permission: String): Boolean { - return context.checkSelfPermission(permission) == android.content.pm.PackageManager.PERMISSION_GRANTED + return ContextCompat.checkSelfPermission( + context, + permission, + ) == PackageManager.PERMISSION_GRANTED } override suspend fun setPermissionDenied(permission: String, value: Boolean) = permissionsStore.setPermissionDenied(permission, value) diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/action/AndroidPermissionActions.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/action/AndroidPermissionActions.kt index 6370e839a3..405b61a809 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/action/AndroidPermissionActions.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/action/AndroidPermissionActions.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.permissions.impl.action import android.content.Context import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.androidutils.system.openAppSettingsPage +import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject @@ -28,6 +28,6 @@ class AndroidPermissionActions @Inject constructor( @ApplicationContext private val context: Context ) : PermissionActions { override fun openSettings() { - context.openAppSettingsPage() + context.startNotificationSettingsIntent() } } diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTest.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTest.kt new file mode 100644 index 0000000000..7e916c37b5 --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.permissions.impl.troubleshoot + +import android.Manifest +import android.os.Build +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.permissions.api.PermissionStateProvider +import io.element.android.libraries.permissions.impl.R +import io.element.android.libraries.permissions.impl.action.PermissionActions +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class NotificationTroubleshootCheckPermissionTest @Inject constructor( + private val permissionStateProvider: PermissionStateProvider, + private val sdkVersionProvider: BuildVersionSdkIntProvider, + private val permissionActions: PermissionActions, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + override val order: Int = 0 + + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_check_permission_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_check_permission_description), + hasQuickFix = true, + fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY, + ) + + override val state: StateFlow = delegate.state + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val result = if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { + permissionStateProvider.isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS) + } else { + true + } + delegate.done(result) + } + + override suspend fun reset() = delegate.reset() + + override suspend fun quickFix(coroutineScope: CoroutineScope) { + // Do not bother about asking the permission inline, just lead the user to the settings + permissionActions.openSettings() + } +} diff --git a/libraries/permissions/impl/src/main/res/values-be/translations.xml b/libraries/permissions/impl/src/main/res/values-be/translations.xml new file mode 100644 index 0000000000..44c9d8376a --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,5 @@ + + + "Пераканайцеся, што праграма можа паказваць апавяшчэнні." + "Праверце дазволы" + diff --git a/libraries/permissions/impl/src/main/res/values-cs/translations.xml b/libraries/permissions/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..038561d93b --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,5 @@ + + + "Ujistěte se, že aplikace může zobrazovat oznámení." + "Kontrola oprávnění" + diff --git a/libraries/permissions/impl/src/main/res/values-de/translations.xml b/libraries/permissions/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..a0ff0ff417 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,5 @@ + + + "Stelle sicher, dass die Anwendung Benachrichtigungen anzeigen kann." + "Berechtigungen überprüfen" + diff --git a/libraries/permissions/impl/src/main/res/values-fr/translations.xml b/libraries/permissions/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..b2cd018579 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,5 @@ + + + "Vérifie que l’application peut afficher des notifications." + "Vérifier les autorisations" + diff --git a/libraries/permissions/impl/src/main/res/values-hu/translations.xml b/libraries/permissions/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..4afe7f181b --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,5 @@ + + + "Ellenőrizze, hogy az alkalmazás képes-e értesítéseket megjeleníteni." + "Engedélyek ellenőrzése" + diff --git a/libraries/permissions/impl/src/main/res/values-ru/translations.xml b/libraries/permissions/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..d1acff7ebd --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,5 @@ + + + "Убедитесь, что приложение может показывать уведомления." + "Проверка разрешений" + diff --git a/libraries/permissions/impl/src/main/res/values-sk/translations.xml b/libraries/permissions/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..995754907e --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,5 @@ + + + "Uistite sa, že aplikácia dokáže zobrazovať upozornenia." + "Skontrolovať povolenia" + diff --git a/libraries/permissions/impl/src/main/res/values/localazy.xml b/libraries/permissions/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..948b026970 --- /dev/null +++ b/libraries/permissions/impl/src/main/res/values/localazy.xml @@ -0,0 +1,5 @@ + + + "Check that the application can show notifications." + "Check permissions" + diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/action/FakePermissionActions.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/action/FakePermissionActions.kt index fa17329900..f6709a7f3d 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/action/FakePermissionActions.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/action/FakePermissionActions.kt @@ -16,11 +16,14 @@ package io.element.android.libraries.permissions.impl.action -class FakePermissionActions : PermissionActions { +class FakePermissionActions( + val openSettingsAction: () -> Unit = {} +) : PermissionActions { var openSettingsCalled = false private set override fun openSettings() { + openSettingsAction() openSettingsCalled = true } } diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTestTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTestTest.kt new file mode 100644 index 0000000000..b38d00ee22 --- /dev/null +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/troubleshoot/NotificationTroubleshootCheckPermissionTestTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.permissions.impl.troubleshoot + +import android.os.Build +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.permissions.impl.FakePermissionStateProvider +import io.element.android.libraries.permissions.impl.action.FakePermissionActions +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class NotificationTroubleshootCheckPermissionTestTest { + @Test + fun `test NotificationTroubleshootCheckPermissionTest below TIRAMISU success`() = runTest { + val sut = NotificationTroubleshootCheckPermissionTest( + permissionStateProvider = FakePermissionStateProvider(), + sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU - 1), + permissionActions = FakePermissionActions(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } + + @Test + fun `test NotificationTroubleshootCheckPermissionTest TIRAMISU success`() = runTest { + val sut = NotificationTroubleshootCheckPermissionTest( + permissionStateProvider = FakePermissionStateProvider(), + sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU), + permissionActions = FakePermissionActions(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } + + @Test + fun `test NotificationTroubleshootCheckPermissionTest TIRAMISU error`() = runTest { + val permissionStateProvider = FakePermissionStateProvider( + permissionGranted = false + ) + val actions = FakePermissionActions( + openSettingsAction = { + permissionStateProvider.setPermissionGranted() + } + ) + val sut = NotificationTroubleshootCheckPermissionTest( + permissionStateProvider = permissionStateProvider, + sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkInt = Build.VERSION_CODES.TIRAMISU), + permissionActions = actions, + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + // Quick fix + launch { + sut.quickFix(this) + // Run the test again (IRL it will be done thanks to the resuming of the application) + sut.run(this) + } + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } +} diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/GetCurrentPushProvider.kt similarity index 71% rename from features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt rename to libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/GetCurrentPushProvider.kt index cd172669cc..0c6ed41929 100644 --- a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/GetCurrentPushProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * Copyright (c) 2024 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,8 @@ * limitations under the License. */ -package io.element.android.features.ftue.api.state +package io.element.android.libraries.push.api -import kotlinx.coroutines.flow.StateFlow - -interface FtueState { - val shouldDisplayFlow: StateFlow - - suspend fun reset() +interface GetCurrentPushProvider { + suspend fun getCurrentPushProvider(): String? } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 5f4736e5ab..abfc328e9f 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -37,6 +37,8 @@ interface PushService { */ suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) - // TODO Move away - suspend fun testPush() + /** + * Return false in case of early error. + */ + suspend fun testPush(): Boolean } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt index c7814a1796..07cf9acf52 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/gateway/PushGatewayFailure.kt @@ -16,6 +16,6 @@ package io.element.android.libraries.push.api.gateway -sealed class PushGatewayFailure : Throwable(cause = null) { - data object PusherRejected : PushGatewayFailure() +sealed class PushGatewayFailure : Exception() { + class PusherRejected : PushGatewayFailure() } diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 7df972c3c3..ee528a4ae7 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.troubleshoot.api) api(projects.libraries.pushproviders.api) api(projects.libraries.pushstore.api) api(projects.libraries.push.api) @@ -69,6 +70,8 @@ dependencies { testImplementation(libs.coil.test) testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.push.test) + testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.tests.testutils) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.toolbox.impl) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultGetCurrentPushProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultGetCurrentPushProvider.kt new file mode 100644 index 0000000000..977d41caca --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultGetCurrentPushProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.api.GetCurrentPushProvider +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.currentSessionId +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultGetCurrentPushProvider @Inject constructor( + private val pushStoreFactory: UserPushStoreFactory, + private val appNavigationStateService: AppNavigationStateService, +) : GetCurrentPushProvider { + override suspend fun getCurrentPushProvider(): String? { + return appNavigationStateService + .appNavigationState + .value + .navigationState + .currentSessionId() + ?.let { pushStoreFactory.getOrCreate(it) } + ?.getPushProviderName() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index e60cd8b014..cff18cfb3d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.push.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.api.GetCurrentPushProvider import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager import io.element.android.libraries.pushproviders.api.Distributor @@ -32,6 +33,7 @@ class DefaultPushService @Inject constructor( private val pushersManager: PushersManager, private val userPushStoreFactory: UserPushStoreFactory, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, + private val getCurrentPushProvider: GetCurrentPushProvider, ) : PushService { override fun notificationStyleChanged() { defaultNotificationDrawerManager.notificationStyleChanged() @@ -47,7 +49,7 @@ class DefaultPushService @Inject constructor( * Get current push provider, compare with provided one, then unregister and register if different, and store change. */ override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { - val userPushStore = userPushStoreFactory.create(matrixClient.sessionId) + val userPushStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId) val currentPushProviderName = userPushStore.getPushProviderName() if (currentPushProviderName != pushProvider.name) { // Unregister previous one if any @@ -58,7 +60,11 @@ class DefaultPushService @Inject constructor( userPushStore.setPushProviderName(pushProvider.name) } - override suspend fun testPush() { - pushersManager.testPush() + override suspend fun testPush(): Boolean { + val currentPushProvider = getCurrentPushProvider.getCurrentPushProvider() + val pushProvider = pushProviders.find { it.name == currentPushProvider } ?: return false + val config = pushProvider.getCurrentUserPushConfig() ?: return false + pushersManager.testPush(config) + return true } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index d4424da492..4306072e48 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -23,9 +23,11 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret @@ -45,16 +47,14 @@ class PushersManager @Inject constructor( private val pushClientSecret: PushClientSecret, private val userPushStoreFactory: UserPushStoreFactory, ) : PusherSubscriber { - // TODO Move this to the PushProvider API - suspend fun testPush() { + suspend fun testPush(config: CurrentUserPushConfig) { pushGatewayNotifyRequest.execute( PushGatewayNotifyRequest.Params( - // unifiedPushHelper.getPushGateway() ?: return - url = "TODO", + url = config.url, appId = PushConfig.PUSHER_APP_ID, - // unifiedPushHelper.getEndpointOrToken().orEmpty() - pushKey = "TODO", - eventId = TEST_EVENT_ID + pushKey = config.pushKey, + eventId = TEST_EVENT_ID, + roomId = TEST_ROOM_ID, ) ) } @@ -63,7 +63,7 @@ class PushersManager @Inject constructor( * Register a pusher to the server if not done yet. */ override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { - val userDataStore = userPushStoreFactory.create(matrixClient.sessionId) + val userDataStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId) if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { Timber.tag(loggerTag.value) .d("Unnecessary to register again the same pusher, but do it in case the pusher has been removed from the server") @@ -112,5 +112,6 @@ class PushersManager @Inject constructor( companion object { val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID") + val TEST_ROOM_ID = RoomId("!room:domain") } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 3b2ac19313..8a285e3b32 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType @@ -68,6 +69,7 @@ class NotifiableEventResolver @Inject constructor( private val matrixClientProvider: MatrixClientProvider, private val notificationMediaRepoFactory: NotificationMediaRepo.Factory, @ApplicationContext private val context: Context, + private val permalinkParser: PermalinkParser, ) { suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { // Restore session @@ -252,7 +254,7 @@ class NotifiableEventResolver @Inject constructor( is ImageMessageType -> messageType.body is StickerMessageType -> messageType.body is NoticeMessageType -> messageType.body - is TextMessageType -> messageType.toPlainText() + is TextMessageType -> messageType.toPlainText(permalinkParser = permalinkParser) is VideoMessageType -> messageType.body is LocationMessageType -> messageType.body is OtherMessageType -> messageType.body diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt index 2cb01ba2f7..04202bbb2f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDisplayer.kt @@ -17,7 +17,6 @@ package io.element.android.libraries.push.impl.notifications import android.Manifest -import android.annotation.SuppressLint import android.app.Notification import android.content.Context import android.content.pm.PackageManager @@ -32,12 +31,13 @@ class NotificationDisplayer @Inject constructor( ) { private val notificationManager = NotificationManagerCompat.from(context) - fun showNotificationMessage(tag: String?, id: Int, notification: Notification) { + fun showNotificationMessage(tag: String?, id: Int, notification: Notification): Boolean { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { Timber.w("Not allowed to notify.") - return + return false } notificationManager.notify(tag, id, notification) + return true } fun cancelNotificationMessage(tag: String?, id: Int) { @@ -53,15 +53,21 @@ class NotificationDisplayer @Inject constructor( } } - @SuppressLint("LaunchActivityFromNotification") - fun displayDiagnosticNotification(notification: Notification) { - showNotificationMessage( + fun displayDiagnosticNotification(notification: Notification): Boolean { + return showNotificationMessage( tag = "DIAGNOSTIC", id = NOTIFICATION_ID_DIAGNOSTIC, notification = notification ) } + fun dismissDiagnosticNotification() { + cancelNotificationMessage( + tag = "DIAGNOSTIC", + id = NOTIFICATION_ID_DIAGNOSTIC + ) + } + /** * Cancel the foreground notification service. */ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt index 152ed0a03e..0bc3e69bfb 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiver.kt @@ -19,9 +19,15 @@ package io.element.android.libraries.push.impl.notifications import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.push.impl.troubleshoot.NotificationClickHandler +import javax.inject.Inject class TestNotificationReceiver : BroadcastReceiver() { + @Inject lateinit var notificationClickHandler: NotificationClickHandler + override fun onReceive(context: Context, intent: Intent) { - // TODO The test notification has been clicked, notify the ui + context.bindings().inject(this) + notificationClickHandler.handleNotificationClick() } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiverBinding.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiverBinding.kt new file mode 100644 index 0000000000..6390bff885 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/TestNotificationReceiverBinding.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import com.squareup.anvil.annotations.ContributesTo +import io.element.android.libraries.di.AppScope + +@ContributesTo(AppScope::class) +interface TestNotificationReceiverBinding { + fun inject(service: TestNotificationReceiver) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index 2beddc53f9..67c8973d13 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -299,6 +299,7 @@ class NotificationCreator @Inject constructor( } fun createDiagnosticNotification(): Notification { + val intent = pendingIntentFactory.createTestPendingIntent() return NotificationCompat.Builder(context, notificationChannels.getChannelIdForTest()) .setContentTitle(buildMeta.applicationName) .setContentText(stringProvider.getString(R.string.notification_test_push_notification_content)) @@ -308,7 +309,8 @@ class NotificationCreator @Inject constructor( .setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_STATUS) .setAutoCancel(true) - .setContentIntent(pendingIntentFactory.createTestPendingIntent()) + .setContentIntent(intent) + .setDeleteIntent(intent) .build() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/permission/NotificationPermissionManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/permission/NotificationPermissionManager.kt deleted file mode 100644 index 7496b3a16b..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/permission/NotificationPermissionManager.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.push.impl.permission - -import android.Manifest -import android.app.Activity -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat -import io.element.android.libraries.di.ApplicationContext -import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider -import javax.inject.Inject - -// TODO EAx move -class NotificationPermissionManager @Inject constructor( - private val sdkIntProvider: BuildVersionSdkIntProvider, - @ApplicationContext private val context: Context, -) { - @RequiresApi(Build.VERSION_CODES.TIRAMISU) - fun isPermissionGranted(): Boolean { - return ContextCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - } - - /* - fun eventuallyRequestPermission( - activity: Activity, - requestPermissionLauncher: ActivityResultLauncher>, - showRationale: Boolean = true, - ignorePreference: Boolean = false, - ) { - if (!sdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) return - // if (!vectorPreferences.areNotificationEnabledForDevice() && !ignorePreference) return - checkPermissions( - listOf(Manifest.permission.POST_NOTIFICATIONS), - activity, - activityResultLauncher = requestPermissionLauncher, - if (showRationale) R.string.permissions_rationale_msg_notification else 0 - ) - } - */ - - fun eventuallyRevokePermission( - activity: Activity, - ) { - if (!sdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) return - activity.revokeSelfPermissionOnKill(Manifest.permission.POST_NOTIFICATIONS) - } -} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index a930db2708..2c9abb77b3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.push.impl.PushersManager import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.store.DefaultPushDataStore +import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushstore.api.UserPushStoreFactory @@ -51,6 +52,7 @@ class DefaultPushHandler @Inject constructor( // private val actionIds: NotificationActionIds, private val buildMeta: BuildMeta, private val matrixAuthenticationService: MatrixAuthenticationService, + private val diagnosticPushHandler: DiagnosticPushHandler, ) : PushHandler { private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -75,8 +77,7 @@ class DefaultPushHandler @Inject constructor( // Diagnostic Push if (pushData.eventId == PushersManager.TEST_EVENT_ID) { - // val intent = Intent(actionIds.push) - // TODO The test push has been received, notify the ui + diagnosticPushHandler.handlePush() return } @@ -121,7 +122,7 @@ class DefaultPushHandler @Inject constructor( return } - val userPushStore = userPushStoreFactory.create(userId) + val userPushStore = userPushStoreFactory.getOrCreate(userId) if (!userPushStore.getNotificationEnabledForDevice().first()) { // TODO We need to check if this is an incoming call Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt index 9e52d94049..5e341e3286 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt @@ -23,6 +23,8 @@ import kotlinx.serialization.Serializable internal data class PushGatewayNotification( @SerialName("event_id") val eventId: String, + @SerialName("room_id") + val roomId: String, /** * Required. This is an array of devices that the notification should be sent to. */ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt index 7130e38d6e..e8c01493ab 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.push.impl.pushgateway import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.push.api.gateway.PushGatewayFailure import javax.inject.Inject @@ -27,7 +28,8 @@ class PushGatewayNotifyRequest @Inject constructor( val url: String, val appId: String, val pushKey: String, - val eventId: EventId + val eventId: EventId, + val roomId: RoomId, ) suspend fun execute(params: Params) { @@ -40,6 +42,7 @@ class PushGatewayNotifyRequest @Inject constructor( PushGatewayNotifyBody( PushGatewayNotification( eventId = params.eventId.value, + roomId = params.roomId.value, devices = listOf( PushGatewayDevice( params.appId, @@ -51,7 +54,7 @@ class PushGatewayNotifyRequest @Inject constructor( ) if (response.rejectedPushKeys.contains(params.pushKey)) { - throw PushGatewayFailure.PusherRejected + throw PushGatewayFailure.PusherRejected() } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTest.kt new file mode 100644 index 0000000000..3e7fe1ae59 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.api.GetCurrentPushProvider +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class CurrentPushProviderTest @Inject constructor( + private val getCurrentPushProvider: GetCurrentPushProvider, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + override val order = 110 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_description), + fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY, + ) + override val state: StateFlow = delegate.state + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val provider = getCurrentPushProvider.getCurrentPushProvider() + if (provider != null) { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_success, provider), + status = NotificationTroubleshootTestState.Status.Success + ) + } else { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_current_push_provider_failure), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } + } + + override suspend fun reset() = delegate.reset() +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/DiagnosticPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/DiagnosticPushHandler.kt new file mode 100644 index 0000000000..21b78161d9 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/DiagnosticPushHandler.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import javax.inject.Inject + +@SingleIn(AppScope::class) +class DiagnosticPushHandler @Inject constructor() { + private val _state = MutableSharedFlow() + val state: SharedFlow = _state + + suspend fun handlePush() { + _state.emit(Unit) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationClickHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationClickHandler.kt new file mode 100644 index 0000000000..29f5fe0b9f --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationClickHandler.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import javax.inject.Inject + +@SingleIn(AppScope::class) +class NotificationClickHandler @Inject constructor() { + private val _state = MutableSharedFlow(extraBufferCapacity = 1) + val state: SharedFlow = _state + + fun handleNotificationClick() { + _state.tryEmit(Unit) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt new file mode 100644 index 0000000000..8de8304c2d --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.notifications.NotificationDisplayer +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import timber.log.Timber +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +@ContributesMultibinding(AppScope::class) +class NotificationTest @Inject constructor( + private val notificationCreator: NotificationCreator, + private val notificationDisplayer: NotificationDisplayer, + private val notificationClickHandler: NotificationClickHandler, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + override val order = 50 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_description), + fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY, + ) + override val state: StateFlow = delegate.state + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val notification = notificationCreator.createDiagnosticNotification() + val result = notificationDisplayer.displayDiagnosticNotification(notification) + if (result) { + coroutineScope.listenToNotificationClick() + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_waiting), + status = NotificationTroubleshootTestState.Status.WaitingForUser + ) + } else { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_permission_failure), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } + } + + private fun CoroutineScope.listenToNotificationClick() = launch { + val job = launch { + notificationClickHandler.state.first() + Timber.d("Notification clicked!") + } + runCatching { + withTimeout(30.seconds) { + job.join() + } + }.fold( + onSuccess = { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_success), + status = NotificationTroubleshootTestState.Status.Success + ) + }, + onFailure = { + job.cancel() + notificationDisplayer.dismissDiagnosticNotification() + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_display_notification_failure), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } + ) + }.invokeOnCompletion { + // Ensure that the notification is cancelled when the screen is left + notificationDisplayer.dismissDiagnosticNotification() + } + + override suspend fun reset() = delegate.reset() +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt new file mode 100644 index 0000000000..42a6394fc9 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.api.gateway.PushGatewayFailure +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.api.strings.StringProvider +import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import timber.log.Timber +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds + +@ContributesMultibinding(AppScope::class) +class PushLoopbackTest @Inject constructor( + private val pushService: PushService, + private val diagnosticPushHandler: DiagnosticPushHandler, + private val clock: SystemClock, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + override val order = 500 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_description), + ) + override val state: StateFlow = delegate.state + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val startTime = clock.epochMillis() + val completable = CompletableDeferred() + val job = coroutineScope.launch { + diagnosticPushHandler.state.first() + completable.complete(clock.epochMillis() - startTime) + } + val testPushResult = try { + pushService.testPush() + } catch (pusherRejected: PushGatewayFailure.PusherRejected) { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_1), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + job.cancel() + return + } catch (e: Exception) { + Timber.e(e, "Failed to test push") + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_2, e.message), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + job.cancel() + return + } + if (!testPushResult) { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_3), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + job.cancel() + return + } + runCatching { + withTimeout(10.seconds) { + completable.await() + } + }.fold( + onSuccess = { duration -> + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_success, duration), + status = NotificationTroubleshootTestState.Status.Success + ) + }, + onFailure = { + job.cancel() + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_push_loop_back_failure_4), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } + ) + } + + override suspend fun reset() = delegate.reset() +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTest.kt new file mode 100644 index 0000000000..190cf8fe98 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.impl.R +import io.element.android.libraries.pushproviders.api.PushProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class PushProvidersTest @Inject constructor( + pushProviders: Set<@JvmSuppressWildcards PushProvider>, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + private val sortedPushProvider = pushProviders.sortedBy { it.index } + override val order = 100 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_description), + fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY, + ) + override val state: StateFlow = delegate.state + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val result = sortedPushProvider.isNotEmpty() + if (result) { + delegate.updateState( + description = stringProvider.getQuantityString( + resId = R.plurals.troubleshoot_notifications_test_detect_push_provider_success, + quantity = sortedPushProvider.size, + sortedPushProvider.size, + sortedPushProvider.joinToString { it.name } + ), + status = NotificationTroubleshootTestState.Status.Success + ) + } else { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_detect_push_provider_failure), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } + } + + override suspend fun reset() = delegate.reset() +} diff --git a/libraries/push/impl/src/main/res/values-be/translations.xml b/libraries/push/impl/src/main/res/values-be/translations.xml index 4c7ce25f88..686d10a281 100644 --- a/libraries/push/impl/src/main/res/values-be/translations.xml +++ b/libraries/push/impl/src/main/res/values-be/translations.xml @@ -6,12 +6,12 @@ "Ціхія апавяшчэнні" "%1$s: %2$d паведамленне" - "%1$s: %2$d паведамленняў" + "%1$s: %2$d паведамлення" "%1$s: %2$d паведамленняў" "%d апавяшчэнне" - "%d апавяшчэнняў" + "%d апавяшчэння" "%d апавяшчэнняў" "Апавяшчэнне" @@ -20,28 +20,28 @@ "Адхіліць" "%d запрашэнне" - "%d запрашэнняў" + "%d запрашэння" "%d запрашэнняў" - "Запрасіў вас у чат" - "Згадаў вас: %1$s" + "Запрасіў(-ла) вас у чат" + "Згадаў(-ла) вас: %1$s" "Новыя паведамленні" "%d новае паведамленне" - "%d новых паведамленняў" + "%d новых паведамлення" "%d новых паведамленняў" - "Адрэагаваў на %1$s" + "Адрэагаваў(-ла) на %1$s" "Пазначыць як прачытанае" "Хуткі адказ" - "Запрасіў вас далучыцца да пакоя" + "Запрасіў(-ла) вас далучыцца да пакоя" "Я" "Вы праглядаеце апавяшчэнне! Націсніце мяне!" "%1$s: %2$s" "%1$s: %2$s %3$s" "%d непрачытанае апавяшчэнне" - "%d непрачытаных апавяшчэнняў" + "%d непрачытаных апавяшчэння" "%d непрачытаных апавяшчэнняў" "%1$s і %2$s" @@ -49,11 +49,36 @@ "%1$s у %2$s і %3$s" "%d пакой" - "%d пакояў" + "%d пакоя" "%d пакояў" "Выберыце спосаб атрымання апавяшчэнняў" "Фонавая сінхранізацыя" "Сэрвісы Google" "Службы Google Play не знойдзены. Апавяшчэнні могуць не працаваць належным чынам." + "Атрымаць назву бягучага пастаўшчыка." + "Пастаўшчыкі push-апавяшчэнняў не выбраны." + "Бягучы пастаўшчык push-апавяшчэнняў: %1$s." + "Бягучы пастаўшчык push-апавяшчэнняў" + "Пераканайцеся, што ў праграме ёсць хаця б адзін пастаўшчык push-апавяшчэнняў." + "Пастаўшчыкі push-апавяшчэнняў не знойдзены." + + "Знайшлі %1$d пастаўшчыка push-апавяшчэнняў: %2$s" + "Знайшлі %1$d пастаўшчыкоў push-апавяшчэнняў: %2$s" + "Знайшлі %1$d пастаўшчыкоў push-апавяшчэнняў: %2$s" + + "Выяўленне пастаўшчыкоў push-паслуг" + "Праверце, ці можа праграма паказваць апавяшчэнні." + "Апавяшчэнне не было націснута." + "Немагчыма паказаць апавяшчэнне." + "Апавяшчэнне было націснута!" + "Паказаць апавяшчэнне" + "Націсніце на апавяшчэнне, каб працягнуць тэст." + "Пераканайцеся, што праграма атрымлівае push-апавяшчэнні." + "Памылка: pusher адхіліў запыт." + "Памылка: %1$s." + "Памылка, немагчыма праверыць push-апавяшчэнне." + "Памылка, тайм-аўт у чаканні push-апавяшчэння." + "Зварот цыклу назад заняў %1$d мс." + "Тэст Націсніце кнопку вярнуцца"
diff --git a/libraries/push/impl/src/main/res/values-bg/translations.xml b/libraries/push/impl/src/main/res/values-bg/translations.xml index 5a9f8d2a59..821ca2f5d8 100644 --- a/libraries/push/impl/src/main/res/values-bg/translations.xml +++ b/libraries/push/impl/src/main/res/values-bg/translations.xml @@ -36,4 +36,5 @@ "%d стая" "%d стаи" + "Грешка: %1$s"
diff --git a/libraries/push/impl/src/main/res/values-cs/translations.xml b/libraries/push/impl/src/main/res/values-cs/translations.xml index db4e6e4c6a..fac2a69784 100644 --- a/libraries/push/impl/src/main/res/values-cs/translations.xml +++ b/libraries/push/impl/src/main/res/values-cs/translations.xml @@ -56,4 +56,29 @@ "Synchronizace na pozadí" "Služby Google" "Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně." + "Získat název aktuálního poskytovatele." + "Nebyli vybráni žádní push poskytovatelé." + "Aktuální push poskytovatel: %1$s." + "Aktuální push poskytovatel" + "Ujistěte se, že aplikace má alespoň jednoho push poskytovatele." + "Nebyli nalezeni žádní push poskytovatelé." + + "Nalezen %1$d push poskytovatel: %2$s" + "Nalezeni %1$d push poskytovatelé: %2$s" + "Nalezeno %1$d push poskytovatelů: %2$s" + + "Zjistit push poskytovatele" + "Zkontrolujte, zda aplikace může zobrazit oznámení." + "Na oznámení nebylo kliknuto." + "Oznámení nelze zobrazit." + "Na oznámení bylo kliknuto!" + "Zobrazit oznámení" + "Kliknutím na oznámení pokračujte v testu." + "Ujistěte se, že aplikace přijímá push." + "Chyba: pusher odmítl požadavek." + "Chyba: %1$s." + "Chyba, nelze otestovat push." + "Chyba, časový limit čekání na push." + "Push zpětná smyčka trvala %1$d ms." + "Otestovat push zpětnou smyčku"
diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml index 5476ad2eea..2757e89dea 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -50,4 +50,28 @@ "Hintergrundsynchronisation" "Google-Dienste" "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." + "Ermittele den Namen des aktuellen Anbieters." + "Kein Push-Anbieter ausgewählt." + "Aktueller Push-Anbieter:%1$s." + "Aktueller Push-Anbieter" + "Stelle sicher, dass die Anwendung mindestens einen Push-Anbieter hat." + "Keine Push-Anbieter gefunden." + + "%1$d Push-Anbieter gefunden: %2$s" + "%1$d Push-Anbieter gefunden: %2$s" + + "Push-Anbieter erkennen" + "Prüfe, ob die Anwendung Benachrichtigungen anzeigen kann." + "Die Benachrichtigung wurde nicht angeklickt." + "Die Benachrichtigung kann nicht angezeigt werden." + "Die Benachrichtigung wurde angeklickt!" + "Benachrichtigung anzeigen" + "Bitte klicke auf die Benachrichtigung, um den Test fortzusetzen." + "Stelle sicher, dass die Anwendung Push-Nachrichten empfängt." + "Fehler: Der Pusher hat die Anfrage abgelehnt." + "Fehler:%1$s." + "Fehler: Push kann nicht getestet werden." + "Fehler: Timeout beim Warten auf Push." + "Push-Loop-Back Dauer: %1$d ms." + "Teste Push-Loop-Back"
diff --git a/libraries/push/impl/src/main/res/values-fr/translations.xml b/libraries/push/impl/src/main/res/values-fr/translations.xml index eb6e4eabbc..8501b8ad76 100644 --- a/libraries/push/impl/src/main/res/values-fr/translations.xml +++ b/libraries/push/impl/src/main/res/values-fr/translations.xml @@ -50,4 +50,28 @@ "Synchronisation en arrière-plan" "Services Google" "Aucun service Google Play valide n’a été trouvé. Les notifications peuvent ne pas fonctionner correctement." + "Obtenir le nom du fournisseur de Push actuel." + "Aucun fournisseur de Push n’est sélectionné." + "Fournisseur de Push actuel : %1$s." + "Fournisseur de Push actuel" + "Vérifier que l’application possède au moins un fournisseur de Push." + "Aucun fournisseur de Push n’a été trouvé." + + "%1$d fournisseur de Push détecté : %2$s" + "%1$d fournisseurs de Push détectés : %2$s" + + "Détecter les fournisseurs de Push" + "Vérifier que l’application peut afficher des notifications." + "Vous n’avez pas cliqué sur la notification." + "Impossible d’afficher la notification." + "Vous avez cliqué sur la notification!" + "Affichage des notifications" + "Veuillez cliquer sur la notification pour continuer le test." + "Vérifier que l’application reçoit les Push." + "Erreur : le Pusher a rejeté la demande." + "Erreur :%1$s." + "Erreur, impossible de tester les Push." + "Erreur, le délai d’attente du Push est dépassé." + "La demande d’envoi de Push et sa réception ont pris %1$d ms." + "Tester la réception des Push"
diff --git a/libraries/push/impl/src/main/res/values-hu/translations.xml b/libraries/push/impl/src/main/res/values-hu/translations.xml index eac6103de7..7b0c66aa65 100644 --- a/libraries/push/impl/src/main/res/values-hu/translations.xml +++ b/libraries/push/impl/src/main/res/values-hu/translations.xml @@ -13,7 +13,7 @@ "%d értesítés" "Értesítés" - "** Nem sikerült elküldeni – kérlek nyisd meg a szobát" + "** Nem sikerült elküldeni – nyissa meg a szobát" "Csatlakozás" "Elutasítás" @@ -30,9 +30,9 @@ "Ezzel reagált: %1$s" "Megjelölés olvasottként" "Gyors válasz" - "Meghívott, hogy csatlakozz a szobához" + "Meghívta, hogy csatlakozzon a szobához" "Én" - "Az értesítést nézed! Kattints ide!" + "Az értesítést nézi! Kattintson ide!" "%1$s: %2$s" "%1$s: %2$s %3$s" @@ -46,8 +46,32 @@ "%d szoba" "%d szoba" - "Válaszd ki az értesítések fogadásának módját" + "Válassza ki az értesítések fogadási módját" "Háttérszinkronizálás" "Google szolgáltatások" "A Google Play szolgáltatások nem találhatók. Előfordulhat, hogy az értesítések nem működnek megfelelően." + "A jelenlegi szolgáltató nevének lekérdezése." + "Nincs kiválasztva leküldéses értesítési szolgáltató." + "Jelenlegi leküldéses értesítési szolgáltató: %1$s." + "Jelenlegi leküldéses értesítési szolgáltató" + "Győződjön meg arról, hogy az alkalmazás legalább egy leküldéses értesítési szolgáltatóval rendelkezik." + "Nem található leküldéses értesítési szolgáltató." + + "%1$d leküldéses szolgáltató találva: %2$s" + "%1$d leküldéses szolgáltató találva: %2$s" + + "Leküldéses értesítési szolgáltatók észlelése" + "Ellenőrizze, hogy az alkalmazás képes-e megjeleníteni az értesítést." + "Az értesítésre nem kattintottak rá." + "Az értesítés nem jeleníthető meg." + "Az értesítésre rákattintottak!" + "Értesítés megjelenítése" + "A teszt folytatásához kattintson az értesítésre." + "Győződjön meg arról, hogy az alkalmazás megkapja-e a leküldéses értesítést." + "Hiba: a leküldő elutasította a kérést." + "Hiba: %1$s." + "Hiba, nem lehet tesztelni a leküldéses értesítést." + "Hiba, időtúllépés a leküldéses értesítésre való várakozás során." + "A leküldéses értesítés folyamata %1$d ezredmásodpercig tartott." + "Tesztelje a leküldéses értesítés folyamatát"
diff --git a/libraries/push/impl/src/main/res/values-in/translations.xml b/libraries/push/impl/src/main/res/values-in/translations.xml index 1a81220eeb..b933330be1 100644 --- a/libraries/push/impl/src/main/res/values-in/translations.xml +++ b/libraries/push/impl/src/main/res/values-in/translations.xml @@ -44,4 +44,27 @@ "Sinkronisasi latar belakang" "Layanan Google" "Tidak ditemukan Layanan Google Play yang valid. Pemberitahuan mungkin tidak berfungsi dengan baik." + "Dapatkan nama penyedia saat ini." + "Tidak ada penyedia notifikasi dorongan yang dipilih." + "Penyedia notifikasi dorongan saat ini: %1$s." + "Penyedia notifikasi dorongan saat ini" + "Pastikan aplikasi memiliki setidaknya satu penyedia notifikasi dorongan." + "Tidak ada penyedia notifikasi dorongan yang ditemukan." + + "Ditemukan %1$d penyedia notifikasi dorongan: %2$s" + + "Deteksi penyedia notifikasi dorongan" + "Periksa apakah aplikasi dapat menampilkan notifikasi." + "Notifikasi belum diklik." + "Tidak dapat menampilkan notifikasi." + "Notifikasi telah diklik!" + "Tampilan notifikasi" + "Silakan klik pada notifikasi untuk melanjutkan tes." + "Pastikan aplikasi menerima notifikasi dorongan." + "Kesalahan: pendorong telah menolak permintaan." + "Kesalahan: %1$s." + "Terjadi kesalahan, tidak dapat menguji notifikasi dorongan." + "Terjadi kesalahan, melebihi batas waktu menunggu notifikasi dorongan." + "Ulangan notifikasi dorongan membutuhkan %1$d ms." + "Uji ulangan notifikasi dorongan lagi"
diff --git a/libraries/push/impl/src/main/res/values-ru/translations.xml b/libraries/push/impl/src/main/res/values-ru/translations.xml index 8881d488ba..65242651b2 100644 --- a/libraries/push/impl/src/main/res/values-ru/translations.xml +++ b/libraries/push/impl/src/main/res/values-ru/translations.xml @@ -56,4 +56,29 @@ "Фоновая синхронизация" "Сервисы Google" "Не найдены действующие службы Google Play. Уведомления могут работать некорректно." + "Получение имени текущего поставщика." + "Поставщики push-уведомлений не выбраны." + "Текущий поставщик push-уведомлений: %1$s." + "Текущий поставщик push-уведомлений" + "Убедитесь, что у приложения есть хотя бы один поставщик push-сообщений." + "Поставщики push-уведомлений не найдены." + + "Найден %1$d push-провайдер: %2$s" + "Найдено %1$d push-провайдеров: %2$s" + "Найдено %1$d push-провайдеров: %2$s" + + "Обнаружение поставщиков push-уведомлений" + "Убедитесь, что приложение может отображать уведомление." + "Уведомление не было нажато." + "Невозможно отобразить уведомление." + "Уведомление было нажато!" + "Отобразить уведомление" + "Нажмите на уведомление, чтобы продолжить тест." + "Убедитесь, что приложение получает push-сообщение." + "Ошибка: pusher отклонил запрос." + "Ошибка: %1$s." + "Ошибка, невозможно протестировать отправку." + "Ошибка, тайм-аут ожидания push-уведомления." + "Обратная отправка push-уведомления, заняла %1$d мс." + "Тест обратной отправки push-уведомления"
diff --git a/libraries/push/impl/src/main/res/values-sk/translations.xml b/libraries/push/impl/src/main/res/values-sk/translations.xml index 4517697945..95aed49bd3 100644 --- a/libraries/push/impl/src/main/res/values-sk/translations.xml +++ b/libraries/push/impl/src/main/res/values-sk/translations.xml @@ -56,4 +56,29 @@ "Synchronizácia na pozadí" "Služby Google" "Nenašli sa žiadne platné služby Google Play. Oznámenia nemusia fungovať správne." + "Získaťe názov aktuálneho poskytovateľa." + "Nie sú vybraní žiadni poskytovatelia push." + "Aktuálny poskytovateľ push: %1$s." + "Aktuálny poskytovateľ push" + "Uistite sa, že aplikácia má aspoň jedného poskytovateľa push." + "Nenašli sa žiadni poskytovatelia push." + + "Nájdený %1$d poskytovateľ služby push: %2$s" + "Nájdení %1$d poskytovatelia služby push: %2$s" + "Nájdených %1$d poskytovateľov služby push: %2$s" + + "Zistiť poskytovateľov push" + "Skontrolujte, či aplikácia dokáže zobraziť upozornenie." + "Na oznámenie nebolo kliknuté." + "Nie je možné zobraziť upozornenie." + "Na oznámenie bolo kliknuté!" + "Zobraziť upozornenie" + "Kliknite na upozornenie a pokračujte v teste." + "Uistite sa, že aplikácia prijíma push oznámenia." + "Chyba: pusher odmietol požiadavku." + "Chyba: %1$s." + "Chyba, nie je možné testovať push." + "Chyba, časový limit na push vypršal." + "Push loop back trvalo %1$d ms." + "Testovať Push loop back"
diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index ed49f5edd8..c9d7627d07 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -50,4 +50,28 @@ "Background synchronization" "Google Services" "No valid Google Play Services found. Notifications may not work properly." + "Get the name of the current provider." + "No push providers selected." + "Current push provider: %1$s." + "Current push provider" + "Ensure that the application has at least one push provider." + "No push providers found." + + "Found %1$d push provider: %2$s" + "Found %1$d push providers: %2$s" + + "Detect push providers" + "Check that the application can display notification." + "The notification has not been clicked." + "Cannot display the notification." + "The notification has been clicked!" + "Display notification" + "Please click on the notification to continue the test." + "Ensure that the application is receiving push." + "Error: pusher has rejected the request." + "Error: %1$s." + "Error, cannot test push." + "Error, timeout waiting for push." + "Push loop back took %1$d ms." + "Test Push loop back"
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt index e641029c0d..b84e7b4be3 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -23,10 +23,10 @@ import io.element.android.libraries.matrix.test.A_SPACE_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoaderHolder -import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator -import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator +import io.element.android.libraries.push.impl.notifications.fake.MockkRoomGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fake.MockkSummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.services.appnavstate.api.AppNavigationState @@ -115,12 +115,12 @@ class DefaultNotificationDrawerManagerTest { appNavigationStateService = appNavigationStateService ), notificationRenderer = NotificationRenderer( - NotificationIdProvider(), - NotificationDisplayer(context), - NotificationFactory( - FakeAndroidNotificationFactory().instance, - FakeRoomGroupMessageCreator().instance, - FakeSummaryGroupMessageCreator().instance, + notificationIdProvider = NotificationIdProvider(), + notificationDisplayer = NotificationDisplayer(context), + notificationFactory = NotificationFactory( + notificationCreator = MockkNotificationCreator().instance, + roomGroupMessageCreator = MockkRoomGroupMessageCreator().instance, + summaryGroupMessageCreator = MockkSummaryGroupMessageCreator().instance, ) ), notificationEventPersistence = InMemoryNotificationEventPersistence(initialData = initialData), diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt index 02da10a351..a8626766e5 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt @@ -25,7 +25,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SPACE_ID import io.element.android.libraries.matrix.test.A_THREAD_ID -import io.element.android.libraries.push.impl.notifications.fake.FakeOutdatedEventDetector +import io.element.android.libraries.push.impl.notifications.fake.MockkOutdatedEventDetector import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent @@ -42,7 +42,7 @@ private val VIEWING_A_ROOM = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_I private val VIEWING_A_THREAD = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID) class NotifiableEventProcessorTest { - private val outdatedDetector = FakeOutdatedEventDetector() + private val mockkOutdatedDetector = MockkOutdatedEventDetector() @Test fun `given simple events when processing then keep simple events`() { @@ -97,7 +97,7 @@ class NotifiableEventProcessorTest { @Test fun `given out of date message event when processing then removes message event`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)) - outdatedDetector.givenEventIsOutOfDate(events[0]) + mockkOutdatedDetector.givenEventIsOutOfDate(events[0]) val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM) @@ -113,7 +113,7 @@ class NotifiableEventProcessorTest { @Test fun `given in date message event when processing then keep message event`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)) - outdatedDetector.givenEventIsInDate(events[0]) + mockkOutdatedDetector.givenEventIsInDate(events[0]) val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM) val result = eventProcessor.process(events, renderedEvents = emptyList()) @@ -128,7 +128,7 @@ class NotifiableEventProcessorTest { @Test fun `given viewing the same room main timeline when processing main timeline message event then removes message`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = null)) - events.forEach { outdatedDetector.givenEventIsOutOfDate(it) } + events.forEach { mockkOutdatedDetector.givenEventIsOutOfDate(it) } val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM) val result = eventProcessor.process(events, renderedEvents = emptyList()) @@ -143,7 +143,7 @@ class NotifiableEventProcessorTest { @Test fun `given viewing the same thread timeline when processing thread message event then removes message`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID)) - events.forEach { outdatedDetector.givenEventIsOutOfDate(it) } + events.forEach { mockkOutdatedDetector.givenEventIsOutOfDate(it) } val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD) val result = eventProcessor.process(events, renderedEvents = emptyList()) @@ -158,7 +158,7 @@ class NotifiableEventProcessorTest { @Test fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID)) - outdatedDetector.givenEventIsInDate(events[0]) + mockkOutdatedDetector.givenEventIsInDate(events[0]) val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM) val result = eventProcessor.process(events, renderedEvents = emptyList()) @@ -173,7 +173,7 @@ class NotifiableEventProcessorTest { @Test fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)) - outdatedDetector.givenEventIsInDate(events[0]) + mockkOutdatedDetector.givenEventIsInDate(events[0]) val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD) val result = eventProcessor.process(events, renderedEvents = emptyList()) @@ -213,8 +213,8 @@ class NotifiableEventProcessorTest { navigationState: NavigationState ): NotifiableEventProcessor { return NotifiableEventProcessor( - outdatedDetector.instance, - FakeAppNavigationStateService(MutableStateFlow(AppNavigationState(navigationState, isInForeground))), + outdatedDetector = mockkOutdatedDetector.instance, + appNavigationStateService = FakeAppNavigationStateService(MutableStateFlow(AppNavigationState(navigationState, isInForeground))), ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt index 4e491a0def..b2f954091e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.notification.FakeNotificationService +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationMediaRepo import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent @@ -549,6 +550,7 @@ class NotifiableEventResolverTest { matrixClientProvider = matrixClientProvider, notificationMediaRepoFactory = notificationMediaRepoFactory, context = context, + permalinkParser = FakePermalinkParser(), ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt index 50ee91f448..6a211fc446 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt @@ -22,10 +22,10 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader -import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator -import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator +import io.element.android.libraries.push.impl.notifications.fake.MockkRoomGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fake.MockkSummaryGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent @@ -41,19 +41,19 @@ private val A_MESSAGE_EVENT = aNotifiableMessageEvent(eventId = AN_EVENT_ID, roo @RunWith(RobolectricTestRunner::class) class NotificationFactoryTest { - private val androidNotificationFactory = FakeAndroidNotificationFactory() - private val roomGroupMessageCreator = FakeRoomGroupMessageCreator() - private val summaryGroupMessageCreator = FakeSummaryGroupMessageCreator() + private val mockkNotificationCreator = MockkNotificationCreator() + private val mockkRoomGroupMessageCreator = MockkRoomGroupMessageCreator() + private val mockkSummaryGroupMessageCreator = MockkSummaryGroupMessageCreator() private val notificationFactory = NotificationFactory( - androidNotificationFactory.instance, - roomGroupMessageCreator.instance, - summaryGroupMessageCreator.instance + notificationCreator = mockkNotificationCreator.instance, + roomGroupMessageCreator = mockkRoomGroupMessageCreator.instance, + summaryGroupMessageCreator = mockkSummaryGroupMessageCreator.instance ) @Test fun `given a room invitation when mapping to notification then is Append`() = testWith(notificationFactory) { - val expectedNotification = androidNotificationFactory.givenCreateRoomInvitationNotificationFor(AN_INVITATION_EVENT) + val expectedNotification = mockkNotificationCreator.givenCreateRoomInvitationNotificationFor(AN_INVITATION_EVENT) val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, AN_INVITATION_EVENT)) val result = roomInvitation.toNotifications() @@ -90,7 +90,7 @@ class NotificationFactoryTest { @Test fun `given a simple event when mapping to notification then is Append`() = testWith(notificationFactory) { - val expectedNotification = androidNotificationFactory.givenCreateSimpleInvitationNotificationFor(A_SIMPLE_EVENT) + val expectedNotification = mockkNotificationCreator.givenCreateSimpleInvitationNotificationFor(A_SIMPLE_EVENT) val roomInvitation = listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_SIMPLE_EVENT)) val result = roomInvitation.toNotifications() @@ -128,7 +128,7 @@ class NotificationFactoryTest { @Test fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) { val events = listOf(A_MESSAGE_EVENT) - val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor( + val expectedNotification = mockkRoomGroupMessageCreator.givenCreatesRoomMessageFor( MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), events, A_ROOM_ID @@ -197,7 +197,7 @@ class NotificationFactoryTest { ) ) val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted"))) - val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor( + val expectedNotification = mockkRoomGroupMessageCreator.givenCreatesRoomMessageFor( MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), withRedactedRemoved, A_ROOM_ID, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt index 4780ae3914..21fc1b4fca 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt @@ -22,8 +22,8 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.push.impl.notifications.fake.FakeImageLoader -import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer -import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory +import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationDisplayer +import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationFactory import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.mockk.mockk import kotlinx.coroutines.test.runTest @@ -51,14 +51,14 @@ private val ONE_SHOT_META = OneShotNotification.Append.Meta(key = "ignored", sum @RunWith(RobolectricTestRunner::class) class NotificationRendererTest { - private val notificationDisplayer = FakeNotificationDisplayer() - private val notificationFactory = FakeNotificationFactory() + private val mockkNotificationDisplayer = MockkNotificationDisplayer() + private val mockkNotificationFactory = MockkNotificationFactory() private val notificationIdProvider = NotificationIdProvider() private val notificationRenderer = NotificationRenderer( notificationIdProvider = notificationIdProvider, - notificationDisplayer = notificationDisplayer.instance, - notificationFactory = notificationFactory.instance, + notificationDisplayer = mockkNotificationDisplayer.instance, + notificationFactory = mockkNotificationFactory.instance, ) @Test @@ -67,8 +67,8 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifySummaryCancelled() - notificationDisplayer.verifyNoOtherInteractions() + mockkNotificationDisplayer.verifySummaryCancelled() + mockkNotificationDisplayer.verifyNoOtherInteractions() } @Test @@ -77,7 +77,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)) cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID)) } @@ -89,7 +89,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID)) showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification) } @@ -108,7 +108,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { showNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomMessagesNotificationId(A_SESSION_ID), A_NOTIFICATION) showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification) } @@ -120,7 +120,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)) cancelNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID)) } @@ -132,7 +132,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID)) showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification) } @@ -151,7 +151,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { showNotificationMessage(tag = AN_EVENT_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID), A_NOTIFICATION) showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification) } @@ -163,7 +163,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID)) cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID)) } @@ -175,7 +175,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { cancelNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomInvitationNotificationId(A_SESSION_ID)) showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification) } @@ -194,7 +194,7 @@ class NotificationRendererTest { renderEventsAsNotifications() - notificationDisplayer.verifyInOrder { + mockkNotificationDisplayer.verifyInOrder { showNotificationMessage(tag = A_ROOM_ID.value, notificationIdProvider.getRoomEventNotificationId(A_SESSION_ID), A_NOTIFICATION) showNotificationMessage(tag = null, notificationIdProvider.getSummaryNotificationId(A_SESSION_ID), A_SUMMARY_NOTIFICATION.notification) } @@ -221,7 +221,7 @@ class NotificationRendererTest { useCompleteNotificationFormat: Boolean = USE_COMPLETE_NOTIFICATION_FORMAT, summaryNotification: SummaryNotification = A_SUMMARY_NOTIFICATION ) { - notificationFactory.givenNotificationsFor( + mockkNotificationFactory.givenNotificationsFor( groupedEvents = A_PROCESSED_EVENTS, matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL), useCompleteNotificationFormat = useCompleteNotificationFormat, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationCreator.kt similarity index 85% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationCreator.kt index 6637b64979..205ba058e6 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationCreator.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import io.mockk.every import io.mockk.mockk -class FakeAndroidNotificationFactory { +class MockkNotificationCreator { val instance = mockk() fun givenCreateRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification { @@ -37,4 +37,10 @@ class FakeAndroidNotificationFactory { every { instance.createSimpleEventNotification(event) } returns mockNotification return mockNotification } + + fun givenCreateDiagnosticNotification(): Notification { + val mockNotification = mockk() + every { instance.createDiagnosticNotification() } returns mockNotification + return mockNotification + } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationDisplayer.kt similarity index 87% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationDisplayer.kt index 9af681490a..dc55cecfac 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationDisplayer.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationDisplayer.kt @@ -20,13 +20,18 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.push.impl.notifications.NotificationDisplayer import io.element.android.libraries.push.impl.notifications.NotificationIdProvider import io.mockk.confirmVerified +import io.mockk.every import io.mockk.mockk import io.mockk.verify import io.mockk.verifyOrder -class FakeNotificationDisplayer { +class MockkNotificationDisplayer { val instance = mockk(relaxed = true) + fun givenDisplayDiagnosticNotificationResult(result: Boolean) { + every { instance.displayDiagnosticNotification(any()) } returns result + } + fun verifySummaryCancelled() { verify { instance.cancelNotificationMessage(tag = null, NotificationIdProvider().getSummaryNotificationId(A_SESSION_ID)) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationFactory.kt similarity index 98% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationFactory.kt index 9c7755aa9d..6a8410d2cb 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkNotificationFactory.kt @@ -26,7 +26,7 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -class FakeNotificationFactory { +class MockkNotificationFactory { val instance = mockk() fun givenNotificationsFor( diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeOutdatedEventDetector.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkOutdatedEventDetector.kt similarity index 97% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeOutdatedEventDetector.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkOutdatedEventDetector.kt index 03bf7e8491..414f7ae652 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeOutdatedEventDetector.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkOutdatedEventDetector.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven import io.mockk.every import io.mockk.mockk -class FakeOutdatedEventDetector { +class MockkOutdatedEventDetector { val instance = mockk() fun givenEventIsOutOfDate(notifiableEvent: NotifiableEvent) { diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkRoomGroupMessageCreator.kt similarity index 97% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkRoomGroupMessageCreator.kt index a41a4aadc2..389a4f441d 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkRoomGroupMessageCreator.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.mockk.coEvery import io.mockk.mockk -class FakeRoomGroupMessageCreator { +class MockkRoomGroupMessageCreator { val instance = mockk() fun givenCreatesRoomMessageFor( diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkSummaryGroupMessageCreator.kt similarity index 95% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkSummaryGroupMessageCreator.kt index 546cb1e054..8f99651c89 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeSummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/MockkSummaryGroupMessageCreator.kt @@ -19,6 +19,6 @@ package io.element.android.libraries.push.impl.notifications.fake import io.element.android.libraries.push.impl.notifications.SummaryGroupMessageCreator import io.mockk.mockk -class FakeSummaryGroupMessageCreator { +class MockkSummaryGroupMessageCreator { val instance = mockk() } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTestTest.kt new file mode 100644 index 0000000000..7f7beb9d9b --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/CurrentPushProviderTestTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.push.test.FakeGetCurrentPushProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CurrentPushProviderTestTest { + @Test + fun `test CurrentPushProviderTest with a push provider`() = runTest { + val sut = CurrentPushProviderTest( + getCurrentPushProvider = FakeGetCurrentPushProvider("foo"), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + assertThat(lastItem.description).contains("foo") + } + } + + @Test + fun `test CurrentPushProviderTest without push provider`() = runTest { + val sut = CurrentPushProviderTest( + getCurrentPushProvider = FakeGetCurrentPushProvider(null), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + } + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTestTest.kt new file mode 100644 index 0000000000..1351117527 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTestTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationCreator +import io.element.android.libraries.push.impl.notifications.fake.MockkNotificationDisplayer +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class NotificationTestTest { + private val mockkNotificationCreator = MockkNotificationCreator().apply { + givenCreateDiagnosticNotification() + } + private val mockkNotificationDisplayer = MockkNotificationDisplayer().apply { + givenDisplayDiagnosticNotificationResult(true) + } + + private val notificationClickHandler = NotificationClickHandler() + + @Test + fun `test NotificationTest notification cannot be displayed`() = runTest { + mockkNotificationDisplayer.givenDisplayDiagnosticNotificationResult(false) + val sut = createNotificationTest() + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + assertThat(awaitItem().status).isInstanceOf(NotificationTroubleshootTestState.Status.Failure::class.java) + } + } + + @Test + fun `test NotificationTest user does not click on notification`() = runTest { + val sut = createNotificationTest() + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser) + assertThat(awaitItem().status).isInstanceOf(NotificationTroubleshootTestState.Status.Failure::class.java) + } + } + + @Test + fun `test NotificationTest user clicks on notification`() = runTest { + val sut = createNotificationTest() + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.WaitingForUser) + notificationClickHandler.handleNotificationClick() + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } + + private fun createNotificationTest(): NotificationTest { + return NotificationTest( + notificationCreator = mockkNotificationCreator.instance, + notificationDisplayer = mockkNotificationDisplayer.instance, + notificationClickHandler = notificationClickHandler, + stringProvider = FakeStringProvider(), + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt new file mode 100644 index 0000000000..2c1363af78 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTestTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_FAILURE_REASON +import io.element.android.libraries.push.api.gateway.PushGatewayFailure +import io.element.android.libraries.push.test.FakePushService +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PushLoopbackTestTest { + @Test + fun `test PushLoopbackTest timeout - push is not received`() = runTest { + val diagnosticPushHandler = DiagnosticPushHandler() + val sut = PushLoopbackTest( + pushService = FakePushService(), + diagnosticPushHandler = diagnosticPushHandler, + clock = FakeSystemClock(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + } + } + + @Test + fun `test PushLoopbackTest PusherRejected error`() = runTest { + val diagnosticPushHandler = DiagnosticPushHandler() + val sut = PushLoopbackTest( + pushService = FakePushService( + testPushBlock = { + throw PushGatewayFailure.PusherRejected() + } + ), + diagnosticPushHandler = diagnosticPushHandler, + clock = FakeSystemClock(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + } + } + + @Test + fun `test PushLoopbackTest setup error`() = runTest { + val diagnosticPushHandler = DiagnosticPushHandler() + val sut = PushLoopbackTest( + pushService = FakePushService( + testPushBlock = { false } + ), + diagnosticPushHandler = diagnosticPushHandler, + clock = FakeSystemClock(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + } + } + + @Test + fun `test PushLoopbackTest other error`() = runTest { + val diagnosticPushHandler = DiagnosticPushHandler() + val sut = PushLoopbackTest( + pushService = FakePushService( + testPushBlock = { + throw AN_EXCEPTION + } + ), + diagnosticPushHandler = diagnosticPushHandler, + clock = FakeSystemClock(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + assertThat(lastItem.description).contains(A_FAILURE_REASON) + } + } + + @Test + fun `test PushLoopbackTest push is received`() = runTest { + val diagnosticPushHandler = DiagnosticPushHandler() + val sut = PushLoopbackTest( + pushService = FakePushService(testPushBlock = { + diagnosticPushHandler.handlePush() + true + }), + diagnosticPushHandler = diagnosticPushHandler, + clock = FakeSystemClock(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTestTest.kt new file mode 100644 index 0000000000..e58a490715 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushProvidersTestTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.troubleshoot + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.pushproviders.test.FakePushProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PushProvidersTestTest { + @Test + fun `test PushProvidersTest with empty list`() = runTest { + val sut = PushProvidersTest( + pushProviders = emptySet(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + } + } + + @Test + fun `test PushProvidersTest with 2 push providers`() = runTest { + val sut = PushProvidersTest( + pushProviders = setOf( + FakePushProvider(name = "foo"), + FakePushProvider(name = "bar"), + ), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(true)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + assertThat(lastItem.description).contains("2") + assertThat(lastItem.description).contains("foo") + assertThat(lastItem.description).contains("bar") + } + } +} diff --git a/libraries/push/test/build.gradle.kts b/libraries/push/test/build.gradle.kts index 9fccadb9be..7826c3072b 100644 --- a/libraries/push/test/build.gradle.kts +++ b/libraries/push/test/build.gradle.kts @@ -25,5 +25,6 @@ android { dependencies { api(projects.libraries.push.api) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.pushproviders.api) implementation(projects.tests.testutils) } diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakeGetCurrentPushProvider.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakeGetCurrentPushProvider.kt new file mode 100644 index 0000000000..76363c9d99 --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakeGetCurrentPushProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.test + +import io.element.android.libraries.push.api.GetCurrentPushProvider + +class FakeGetCurrentPushProvider( + private val currentPushProvider: String? +) : GetCurrentPushProvider { + override suspend fun getCurrentPushProvider(): String? = currentPushProvider +} diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt new file mode 100644 index 0000000000..969815ec66 --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.test + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.api.PushProvider +import io.element.android.tests.testutils.simulateLongTask + +class FakePushService( + private val testPushBlock: suspend () -> Boolean = { true } +) : PushService { + override fun notificationStyleChanged() { + } + + override fun getAvailablePushProviders(): List { + return emptyList() + } + + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { + } + + override suspend fun testPush(): Boolean = simulateLongTask { + testPushBlock() + } +} diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/CurrentUserPushConfig.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/CurrentUserPushConfig.kt new file mode 100644 index 0000000000..bfd6488904 --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/CurrentUserPushConfig.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.api + +data class CurrentUserPushConfig( + val url: String, + val pushKey: String, +) diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt index 3d8349a117..4e9b818dd4 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt @@ -49,8 +49,5 @@ interface PushProvider { */ suspend fun unregister(matrixClient: MatrixClient) - /** - * Attempt to troubleshoot the push provider. - */ - suspend fun troubleshoot(): Result + suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? } diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 186ab121fd..6e36b92a09 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -40,6 +40,9 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.troubleshoot.api) + implementation(projects.services.toolbox.api) implementation(projects.libraries.pushstore.api) implementation(projects.libraries.pushproviders.api) @@ -51,8 +54,11 @@ dependencies { exclude(group = "com.google.firebase", module = "firebase-measurement-connector") } + testImplementation(libs.coroutines.test) testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.tests.testutils) + testImplementation(projects.services.toolbox.test) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt index 313b9ab706..20d0de4ebf 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt @@ -44,7 +44,7 @@ class FirebaseNewTokenHandler @Inject constructor( sessionStore.getAllSessions().toUserList() .map { SessionId(it) } .forEach { userId -> - val userDataStore = userPushStoreFactory.create(userId) + val userDataStore = userPushStoreFactory.getOrCreate(userId) if (userDataStore.getPushProviderName() == FirebaseConfig.NAME) { matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client -> pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.PUSHER_HTTP_URL) diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt index 4c3d6d3a20..317d49f3b6 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt @@ -16,14 +16,11 @@ package io.element.android.libraries.pushproviders.firebase -import android.content.Context -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability import com.squareup.anvil.annotations.ContributesMultibinding import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PusherSubscriber @@ -34,25 +31,15 @@ private val loggerTag = LoggerTag("FirebasePushProvider", LoggerTag.PushLoggerTa @ContributesMultibinding(AppScope::class) class FirebasePushProvider @Inject constructor( - @ApplicationContext private val context: Context, private val firebaseStore: FirebaseStore, - private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, + private val isPlayServiceAvailable: IsPlayServiceAvailable, ) : PushProvider { override val index = FirebaseConfig.INDEX override val name = FirebaseConfig.NAME override fun isAvailable(): Boolean { - // The PlayServices has to be available - val apiAvailability = GoogleApiAvailability.getInstance() - val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) - return if (resultCode == ConnectionResult.SUCCESS) { - Timber.tag(loggerTag.value).d("Google Play Services is available") - true - } else { - Timber.tag(loggerTag.value).w("Google Play Services is not available") - false - } + return isPlayServiceAvailable.isAvailable() } override fun getDistributors(): List { @@ -73,7 +60,12 @@ class FirebasePushProvider @Inject constructor( pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.PUSHER_HTTP_URL) } - override suspend fun troubleshoot(): Result { - return firebaseTroubleshooter.troubleshoot() + override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? { + return firebaseStore.getFcmToken()?.let { fcmToken -> + CurrentUserPushConfig( + url = FirebaseConfig.PUSHER_HTTP_URL, + pushKey = fcmToken + ) + } } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt index 0342c67462..0614e2065c 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseStore.kt @@ -18,20 +18,28 @@ package io.element.android.libraries.pushproviders.firebase import android.content.SharedPreferences import androidx.core.content.edit +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.DefaultPreferences import javax.inject.Inject /** * This class store the Firebase token in SharedPrefs. */ -class FirebaseStore @Inject constructor( +interface FirebaseStore { + fun getFcmToken(): String? + fun storeFcmToken(token: String?) +} + +@ContributesBinding(AppScope::class) +class DefaultFirebaseStore @Inject constructor( @DefaultPreferences private val sharedPrefs: SharedPreferences, -) { - fun getFcmToken(): String? { +) : FirebaseStore { + override fun getFcmToken(): String? { return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) } - fun storeFcmToken(token: String?) { + override fun storeFcmToken(token: String?) { sharedPrefs.edit { putString(PREFS_KEY_FCM_TOKEN, token) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt index f3efba1a16..6d205c42ae 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt @@ -16,25 +16,28 @@ package io.element.android.libraries.pushproviders.firebase -import android.content.Context -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability import com.google.firebase.messaging.FirebaseMessaging -import io.element.android.libraries.di.ApplicationContext +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine +interface FirebaseTroubleshooter { + suspend fun troubleshoot(): Result +} + /** * This class force retrieving and storage of the Firebase token. */ -class FirebaseTroubleshooter @Inject constructor( - @ApplicationContext private val context: Context, +@ContributesBinding(AppScope::class) +class DefaultFirebaseTroubleshooter @Inject constructor( private val newTokenHandler: FirebaseNewTokenHandler, -) { - suspend fun troubleshoot(): Result { + private val isPlayServiceAvailable: IsPlayServiceAvailable, +) : FirebaseTroubleshooter { + override suspend fun troubleshoot(): Result { return runCatching { val token = retrievedFirebaseToken() newTokenHandler.handle(token) @@ -44,7 +47,7 @@ class FirebaseTroubleshooter @Inject constructor( private suspend fun retrievedFirebaseToken(): String { return suspendCoroutine { continuation -> // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' - if (checkPlayServices(context)) { + if (isPlayServiceAvailable.isAvailable()) { try { FirebaseMessaging.getInstance().token .addOnSuccessListener { token -> @@ -65,15 +68,4 @@ class FirebaseTroubleshooter @Inject constructor( } } } - - /** - * Check the device to make sure it has the Google Play Services APK. If - * it doesn't, display a dialog that allows users to download the APK from - * the Google Play Store or enable it in the device's system settings. - */ - private fun checkPlayServices(context: Context): Boolean { - val apiAvailability = GoogleApiAvailability.getInstance() - val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) - return resultCode == ConnectionResult.SUCCESS - } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt new file mode 100644 index 0000000000..50d9d4fe6f --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/IsPlayServiceAvailable.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase + +import android.content.Context +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import timber.log.Timber +import javax.inject.Inject + +interface IsPlayServiceAvailable { + fun isAvailable(): Boolean +} + +@ContributesBinding(AppScope::class) +class DefaultIsPlayServiceAvailable @Inject constructor( + @ApplicationContext private val context: Context, +) : IsPlayServiceAvailable { + override fun isAvailable(): Boolean { + val apiAvailability = GoogleApiAvailability.getInstance() + val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) + return if (resultCode == ConnectionResult.SUCCESS) { + Timber.d("Google Play Services is available") + true + } else { + Timber.w("Google Play Services is not available") + false + } + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTest.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTest.kt new file mode 100644 index 0000000000..6cca3d21af --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase.troubleshoot + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.pushproviders.firebase.FirebaseConfig +import io.element.android.libraries.pushproviders.firebase.IsPlayServiceAvailable +import io.element.android.libraries.pushproviders.firebase.R +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class FirebaseAvailabilityTest @Inject constructor( + private val isPlayServiceAvailable: IsPlayServiceAvailable, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + override val order = 300 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_description), + visibleWhenIdle = false, + fakeDelay = NotificationTroubleshootTestDelegate.LONG_DELAY, + ) + override val state: StateFlow = delegate.state + + override fun isRelevant(data: TestFilterData): Boolean { + return data.currentPushProviderName == FirebaseConfig.NAME + } + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val result = isPlayServiceAvailable.isAvailable() + if (result) { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_success), + status = NotificationTroubleshootTestState.Status.Success + ) + } else { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_availability_failure), + status = NotificationTroubleshootTestState.Status.Failure(false) + ) + } + } + + override suspend fun reset() = delegate.reset() +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt new file mode 100644 index 0000000000..a465ca3a7b --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase.troubleshoot + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.pushproviders.firebase.FirebaseConfig +import io.element.android.libraries.pushproviders.firebase.FirebaseStore +import io.element.android.libraries.pushproviders.firebase.FirebaseTroubleshooter +import io.element.android.libraries.pushproviders.firebase.R +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class FirebaseTokenTest @Inject constructor( + private val firebaseStore: FirebaseStore, + private val firebaseTroubleshooter: FirebaseTroubleshooter, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + override val order = 310 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_description), + visibleWhenIdle = false, + fakeDelay = NotificationTroubleshootTestDelegate.LONG_DELAY, + ) + override val state: StateFlow = delegate.state + + override fun isRelevant(data: TestFilterData): Boolean { + return data.currentPushProviderName == FirebaseConfig.NAME + } + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val token = firebaseStore.getFcmToken() + if (token != null) { + delegate.updateState( + description = stringProvider.getString( + R.string.troubleshoot_notifications_test_firebase_token_success, + "${token.take(8)}*****" + ), + status = NotificationTroubleshootTestState.Status.Success + ) + } else { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_firebase_token_failure), + status = NotificationTroubleshootTestState.Status.Failure(true) + ) + } + } + + override suspend fun reset() = delegate.reset() + + override suspend fun quickFix(coroutineScope: CoroutineScope) { + delegate.start() + firebaseTroubleshooter.troubleshoot() + run(coroutineScope) + } +} diff --git a/libraries/pushproviders/firebase/src/main/res/values-be/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-be/translations.xml new file mode 100644 index 0000000000..cd9082e913 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-be/translations.xml @@ -0,0 +1,11 @@ + + + "Пераканайцеся, што Firebase даступны." + "Firebase недаступны." + "Firebase даступны." + "Праверыць Firebase" + "Пераканайцеся, што маркер Firebase даступны." + "Маркер Firebase невядомы." + "Маркер Firebase: %1$s." + "Праверце маркер Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-cs/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..e0b7eff47f --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-cs/translations.xml @@ -0,0 +1,11 @@ + + + "Ujistěte se, že je k dispozici Firebase." + "Firebase není k dispozici." + "Firebase je k dispozici." + "Zkontrolovat Firebase" + "Ujistěte se, že je k dispozici Firebase token." + "Firebase token není znám." + "Firebase token: %1$s." + "Zkontrolovat Firebase token" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-de/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..5df74b7284 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-de/translations.xml @@ -0,0 +1,11 @@ + + + "Stelle sicher, dass Firebase verfügbar ist." + "Firebase ist nicht verfügbar." + "Firebase ist verfügbar." + "Überprüfe Firebase" + "Stelle sicher, dass der Firebase Token verfügbar ist." + "Firebase Token ist nicht bekannt." + "Firebase Token: %1$s." + "Prüfe Firebase Token" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-fr/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..75256022a2 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-fr/translations.xml @@ -0,0 +1,11 @@ + + + "Vérification que Firebase est disponible." + "Firebase n’est pas disponible." + "Firebase est disponible." + "Vérification de Firebase" + "Vérifier que le jeton Firebase est disponible." + "Le jeton Firebase n’est pas connu." + "Jeton Firebase :%1$s." + "Vérifier le jeton Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-hu/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..10fa194159 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-hu/translations.xml @@ -0,0 +1,11 @@ + + + "Győződjön meg arról, hogy a Firebase elérhető-e." + "A Firebase nem érhető el." + "A Firebase elérhető." + "Ellenőrizze a Firebase-t" + "Győződjön meg arról, hogy a Firebase-token elérhető." + "A Firebase-token nem ismert." + "Firebase-token: %1$s." + "Ellenőrizze a Firebase-tokent" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-ru/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..4167dd0d36 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-ru/translations.xml @@ -0,0 +1,11 @@ + + + "Убедитесь, что Firebase доступен." + "Firebase недоступен." + "Firebase доступен." + "Проверить Firebase" + "Убедитесь, что токен Firebase доступен." + "Токен Firebase неизвестен." + "Токен Firebase: %1$s." + "Проверить токен Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values-sk/translations.xml b/libraries/pushproviders/firebase/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..312f751ce5 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values-sk/translations.xml @@ -0,0 +1,11 @@ + + + "Uistite sa, že Firebase je k dispozícii." + "Firebase nie je k dispozícii." + "Firebase je k dispozícii." + "Skontrolovať Firebase" + "Uistite sa, že je k dispozícii token Firebase." + "Token Firebase nie je známy." + "Token Firebase: %1$s." + "Skontrolovať token Firebase" + diff --git a/libraries/pushproviders/firebase/src/main/res/values/localazy.xml b/libraries/pushproviders/firebase/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..654ba04134 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values/localazy.xml @@ -0,0 +1,11 @@ + + + "Ensure that Firebase is available." + "Firebase is not available." + "Firebase is available." + "Check Firebase" + "Ensure that Firebase token is available." + "Firebase token is not known." + "Firebase token: %1$s." + "Check Firebase token" + diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTroubleshooter.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTroubleshooter.kt new file mode 100644 index 0000000000..b0dae793cd --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseTroubleshooter.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase + +import io.element.android.tests.testutils.simulateLongTask + +class FakeFirebaseTroubleshooter( + private val troubleShootResult: () -> Result = { Result.success(Unit) } +) : FirebaseTroubleshooter { + override suspend fun troubleshoot(): Result = simulateLongTask { + troubleShootResult() + } +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt new file mode 100644 index 0000000000..f298b9f30e --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/InMemoryFirebaseStore.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase + +class InMemoryFirebaseStore( + private var token: String? = null +) : FirebaseStore { + override fun getFcmToken(): String? = token + + override fun storeFcmToken(token: String?) { + this.token = token + } +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt new file mode 100644 index 0000000000..6f1a3da7cb --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase.troubleshoot + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.pushproviders.firebase.IsPlayServiceAvailable +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class FirebaseAvailabilityTestTest { + @Test + fun `test FirebaseAvailabilityTest success`() = runTest { + val sut = FirebaseAvailabilityTest( + isPlayServiceAvailable = object : IsPlayServiceAvailable { + override fun isAvailable(): Boolean { + return true + } + }, + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } + + @Test + fun `test FirebaseAvailabilityTest failure`() = runTest { + val sut = FirebaseAvailabilityTest( + isPlayServiceAvailable = object : IsPlayServiceAvailable { + override fun isAvailable(): Boolean { + return false + } + }, + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) + } + } +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt new file mode 100644 index 0000000000..2d8de62ad9 --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase.troubleshoot + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter +import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class FirebaseTokenTestTest { + @Test + fun `test FirebaseTokenTest success`() = runTest { + val sut = FirebaseTokenTest( + firebaseStore = InMemoryFirebaseStore(FAKE_TOKEN), + firebaseTroubleshooter = FakeFirebaseTroubleshooter(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + assertThat(lastItem.description).contains(FAKE_TOKEN.take(8)) + assertThat(lastItem.description).doesNotContain(FAKE_TOKEN) + } + } + + @Test + fun `test FirebaseTokenTest error`() = runTest { + val firebaseStore = InMemoryFirebaseStore(null) + val sut = FirebaseTokenTest( + firebaseStore = firebaseStore, + firebaseTroubleshooter = FakeFirebaseTroubleshooter( + troubleShootResult = { + firebaseStore.storeFcmToken(FAKE_TOKEN) + Result.success(Unit) + } + ), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + // Quick fix + sut.quickFix(this) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } + + companion object { + private const val FAKE_TOKEN = "abcdefghijk" + } +} diff --git a/libraries/pushproviders/test/build.gradle.kts b/libraries/pushproviders/test/build.gradle.kts new file mode 100644 index 0000000000..ddb68ed43f --- /dev/null +++ b/libraries/pushproviders/test/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.pushproviders.test" +} + +dependencies { + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.pushproviders.api) +} diff --git a/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt b/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt new file mode 100644 index 0000000000..8d8b94ec19 --- /dev/null +++ b/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.test + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.api.PushProvider + +class FakePushProvider( + override val index: Int = 0, + override val name: String = "aFakePushProvider", + private val isAvailable: Boolean = true, + private val distributors: List = emptyList() +) : PushProvider { + override fun isAvailable(): Boolean = isAvailable + + override fun getDistributors(): List = distributors + + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) { + // No-op + } + + override suspend fun unregister(matrixClient: MatrixClient) { + // No-op + } + + override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? { + return null + } +} diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index a3968f4ecc..d5dcc9727d 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -32,11 +32,14 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.uiStrings) + api(projects.libraries.troubleshoot.api) implementation(projects.libraries.pushstore.api) implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.services.appnavstate.api) implementation(projects.services.toolbox.api) implementation(projects.libraries.network) @@ -50,8 +53,11 @@ dependencies { // UnifiedPush library api(libs.unifiedpush) + testImplementation(libs.coroutines.test) testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.tests.testutils) + testImplementation(projects.services.toolbox.test) } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushConfig.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushConfig.kt index 07c57496db..b1d321f42f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushConfig.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushConfig.kt @@ -23,6 +23,8 @@ object UnifiedPushConfig { */ const val DEFAULT_PUSH_GATEWAY_HTTP_URL: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" + const val UNIFIED_PUSH_DISTRIBUTORS_URL = "https://unifiedpush.org/users/distributors/" + const val INDEX = 1 const val NAME = "UnifiedPush" } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushDistributorProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushDistributorProvider.kt new file mode 100644 index 0000000000..5c9249e8f4 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushDistributorProvider.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.system.getApplicationLabel +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.pushproviders.api.Distributor +import org.unifiedpush.android.connector.UnifiedPush +import javax.inject.Inject + +interface UnifiedPushDistributorProvider { + fun getDistributors(): List +} + +@ContributesBinding(AppScope::class) +class DefaultUnifiedPushDistributorProvider @Inject constructor( + @ApplicationContext private val context: Context, +) : UnifiedPushDistributorProvider { + override fun getDistributors(): List { + val distributors = UnifiedPush.getDistributors(context) + return distributors.mapNotNull { + if (it == context.packageName) { + // Exclude self + null + } else { + Distributor(it, context.getApplicationLabel(it)) + } + } + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt index 2ae8753a1e..4ee637a3ab 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -40,7 +40,7 @@ class UnifiedPushNewGatewayHandler @Inject constructor( val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Unit.also { Timber.w("Unable to retrieve session") } - val userDataStore = userPushStoreFactory.create(userId) + val userDataStore = userPushStoreFactory.getOrCreate(userId) if (userDataStore.getPushProviderName() == UnifiedPushConfig.NAME) { matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client -> pusherSubscriber.registerPusher(client, endpoint, pushGateway) diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt index 5d0a0da7a1..e7ea1841c5 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProvider.kt @@ -16,17 +16,16 @@ package io.element.android.libraries.pushproviders.unifiedpush -import android.content.Context import com.squareup.anvil.annotations.ContributesMultibinding -import io.element.android.libraries.androidutils.system.getApplicationLabel import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret -import org.unifiedpush.android.connector.UnifiedPush +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.currentSessionId import timber.log.Timber import javax.inject.Inject @@ -34,10 +33,12 @@ private val loggerTag = LoggerTag("UnifiedPushProvider", LoggerTag.PushLoggerTag @ContributesMultibinding(AppScope::class) class UnifiedPushProvider @Inject constructor( - @ApplicationContext private val context: Context, + private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider, private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val pushClientSecret: PushClientSecret, + private val unifiedPushStore: UnifiedPushStore, + private val appNavigationStateService: AppNavigationStateService, ) : PushProvider { override val index = UnifiedPushConfig.INDEX override val name = UnifiedPushConfig.NAME @@ -54,15 +55,7 @@ class UnifiedPushProvider @Inject constructor( } override fun getDistributors(): List { - val distributors = UnifiedPush.getDistributors(context) - return distributors.mapNotNull { - if (it == context.packageName) { - // Exclude self - null - } else { - Distributor(it, context.getApplicationLabel(it)) - } - } + return unifiedPushDistributorProvider.getDistributors() } override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) { @@ -75,7 +68,14 @@ class UnifiedPushProvider @Inject constructor( unRegisterUnifiedPushUseCase.execute(clientSecret) } - override suspend fun troubleshoot(): Result { - TODO("Not yet implemented") + override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? { + val currentSession = appNavigationStateService.appNavigationState.value.navigationState.currentSessionId() ?: return null + val clientSecret = pushClientSecret.getSecretForUser(currentSession) + val url = unifiedPushStore.getPushGateway(clientSecret) ?: return null + val pushKey = unifiedPushStore.getEndpoint(clientSecret) ?: return null + return CurrentUserPushConfig( + url = url, + pushKey = pushKey, + ) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/OpenDistributorWebPageAction.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/OpenDistributorWebPageAction.kt new file mode 100644 index 0000000000..dda4292f12 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/OpenDistributorWebPageAction.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot + +import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.system.openUrlInExternalApp +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig +import javax.inject.Inject + +interface OpenDistributorWebPageAction { + fun execute() +} + +@ContributesBinding(AppScope::class) +class DefaultOpenDistributorWebPageAction @Inject constructor( + @ApplicationContext private val context: Context, +) : OpenDistributorWebPageAction { + override fun execute() { + // Open the distributor download page + context.openUrlInExternalApp( + url = UnifiedPushConfig.UNIFIED_PUSH_DISTRIBUTORS_URL, + ) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTest.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTest.kt new file mode 100644 index 0000000000..77a2347429 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.pushproviders.unifiedpush.R +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.services.toolbox.api.strings.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class UnifiedPushTest @Inject constructor( + private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider, + private val openDistributorWebPageAction: OpenDistributorWebPageAction, + private val stringProvider: StringProvider, +) : NotificationTroubleshootTest { + override val order = 400 + private val delegate = NotificationTroubleshootTestDelegate( + defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_title), + defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_description), + visibleWhenIdle = false, + fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY, + ) + override val state: StateFlow = delegate.state + + override fun isRelevant(data: TestFilterData): Boolean { + return data.currentPushProviderName == UnifiedPushConfig.NAME + } + + override suspend fun run(coroutineScope: CoroutineScope) { + delegate.start() + val distributors = unifiedPushDistributorProvider.getDistributors() + if (distributors.isNotEmpty()) { + delegate.updateState( + description = stringProvider.getQuantityString( + resId = R.plurals.troubleshoot_notifications_test_unified_push_success, + quantity = distributors.size, + distributors.size, + distributors.joinToString { it.name } + ), + status = NotificationTroubleshootTestState.Status.Success + ) + } else { + delegate.updateState( + description = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_failure), + status = NotificationTroubleshootTestState.Status.Failure(true) + ) + } + } + + override suspend fun reset() = delegate.reset() + + override suspend fun quickFix(coroutineScope: CoroutineScope) { + openDistributorWebPageAction.execute() + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-be/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-be/translations.xml new file mode 100644 index 0000000000..5fcab0a3f3 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-be/translations.xml @@ -0,0 +1,11 @@ + + + "Пераканайцеся, што размеркавальнікі UnifiedPush даступныя." + "Размеркавальнікі не знойдзены." + + "%1$d знойдзены размеркавальнік: %2$s." + "%1$d знойдзена размеркавальніка: %2$s." + "%1$d знойдзена размеркавальнікаў: %2$s." + + "Праверыць UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-cs/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..d4eeb3ef96 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-cs/translations.xml @@ -0,0 +1,11 @@ + + + "Ujistěte se, že jsou k dispozici distributoři UnifiedPush." + "Nebyli nalezeni žádní push distributoři." + + "Nalezen %1$d distributor: %2$s." + "Nalezeni %1$d distributoři: %2$s." + "Nalezeno %1$d distributorů: %2$s." + + "Zkontrolovat UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-de/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..92a632a39d --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-de/translations.xml @@ -0,0 +1,10 @@ + + + "Stelle sicher, dass UnifiedPush-Verteiler verfügbar sind." + "Keine Push-Verteiler gefunden." + + "%1$d Verteiler gefunden: %2$s." + "%1$d Verteiler gefunden: %2$s." + + "UnifiedPush prüfen" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-fr/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..fe7769da5e --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-fr/translations.xml @@ -0,0 +1,10 @@ + + + "Vérifier qu’au moins un distributeur UnifiedPush est disponible." + "Aucun distributeur UnifiedPush n’a été trouvé." + + "%1$d distributeur détecté :%2$s." + "%1$d distributeurs détectés :%2$s." + + "Vérifier UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-hu/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..7c92f521da --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-hu/translations.xml @@ -0,0 +1,10 @@ + + + "Győződjön meg arról, hogy a UnifiedPush forgalmazói elérhetők." + "Nem található forgalmazó a leküldéses értesítésekhez." + + "%1$d forgalmazó található: %2$s." + "%1$d forgalmazó található: %2$s." + + "Ellenőrizze a UnifiedPush szolgáltatást" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-ru/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..91a4dbc81b --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-ru/translations.xml @@ -0,0 +1,11 @@ + + + "Убедитесь, что дистрибьюторы UnifiedPush доступны." + "Поставщиков push-уведомлений не найдено." + + "%1$d провайдер найден: %2$s." + "%1$d провайдеров найдено: %2$s." + "%1$d провайдеров найдено: %2$s." + + "Проверка UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values-sk/translations.xml b/libraries/pushproviders/unifiedpush/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..3dc876cb5b --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values-sk/translations.xml @@ -0,0 +1,11 @@ + + + "Uistite sa, že sú dostupní distribútori UnifiedPush." + "Nenašli sa žiadni distribútori push." + + "%1$d nájdený distribútor: %2$s." + "%1$d nájdení distribútori: %2$s." + "%1$d nájdených distribútorov: %2$s." + + "Skontrolovať UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/main/res/values/localazy.xml b/libraries/pushproviders/unifiedpush/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..0e16af1f3e --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/res/values/localazy.xml @@ -0,0 +1,10 @@ + + + "Ensure that UnifiedPush distributors are available." + "No push distributors found." + + "%1$d distributor found: %2$s." + "%1$d distributors found: %2$s." + + "Check UnifiedPush" + diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/FakeOpenDistributorWebPageAction.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/FakeOpenDistributorWebPageAction.kt new file mode 100644 index 0000000000..ca91807fb9 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/FakeOpenDistributorWebPageAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot + +class FakeOpenDistributorWebPageAction( + private val executeAction: () -> Unit = {} +) : OpenDistributorWebPageAction { + override fun execute() = executeAction() +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/FakeUnifiedPushDistributorProvider.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/FakeUnifiedPushDistributorProvider.kt new file mode 100644 index 0000000000..e9734956d7 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/FakeUnifiedPushDistributorProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot + +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider + +class FakeUnifiedPushDistributorProvider( + private var getDistributorsResult: List = emptyList() +) : UnifiedPushDistributorProvider { + override fun getDistributors(): List { + return getDistributorsResult + } + + fun setDistributorsResult(list: List) { + getDistributorsResult = list + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt new file mode 100644 index 0000000000..117e8b7457 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class UnifiedPushTestTest { + @Test + fun `test UnifiedPushTest success`() = runTest { + val sut = UnifiedPushTest( + unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider( + getDistributorsResult = listOf( + Distributor("value", "Name"), + ) + ), + openDistributorWebPageAction = FakeOpenDistributorWebPageAction(), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } + + @Test + fun `test UnifiedPushTest error`() = runTest { + val providers = FakeUnifiedPushDistributorProvider() + val sut = UnifiedPushTest( + unifiedPushDistributorProvider = providers, + openDistributorWebPageAction = FakeOpenDistributorWebPageAction( + executeAction = { + providers.setDistributorsResult( + listOf( + Distributor("value", "Name"), + ) + ) + } + ), + stringProvider = FakeStringProvider(), + ) + launch { + sut.run(this) + } + sut.state.test { + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false)) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + val lastItem = awaitItem() + assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(true)) + // Quick fix + launch { + sut.quickFix(this) + sut.run(this) + } + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress) + assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success) + } + } +} diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt index 52e4596ca0..95097845ab 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt @@ -22,5 +22,5 @@ import io.element.android.libraries.matrix.api.core.SessionId * Store data related to push about a user. */ interface UserPushStoreFactory { - fun create(userId: SessionId): UserPushStore + fun getOrCreate(userId: SessionId): UserPushStore } diff --git a/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt b/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt index b7bbf46dc4..7f130459fa 100644 --- a/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt +++ b/libraries/pushstore/impl/src/androidTest/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactoryTest.kt @@ -40,11 +40,11 @@ class DefaultUserPushStoreFactoryTest { val userPushStoreFactory = DefaultUserPushStoreFactory(context, NoOpSessionObserver()) var userPushStore1: UserPushStore? = null val thread1 = thread { - userPushStore1 = userPushStoreFactory.create(sessionId) + userPushStore1 = userPushStoreFactory.getOrCreate(sessionId) } var userPushStore2: UserPushStore? = null val thread2 = thread { - userPushStore2 = userPushStoreFactory.create(sessionId) + userPushStore2 = userPushStoreFactory.getOrCreate(sessionId) } thread1.join() thread2.join() diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index 8c85dca80c..3c65c76a01 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -41,7 +41,7 @@ class DefaultUserPushStoreFactory @Inject constructor( // We can have only one class accessing a single data store, so keep a cache of them. private val cache = ConcurrentHashMap() - override fun create(userId: SessionId): UserPushStore { + override fun getOrCreate(userId: SessionId): UserPushStore { return cache.getOrPut(userId) { UserPushStoreDataStore( context = context, @@ -60,6 +60,6 @@ class DefaultUserPushStoreFactory @Inject constructor( override suspend fun onSessionDeleted(userId: String) { // Delete the store - create(SessionId(userId)).reset() + getOrCreate(SessionId(userId)).reset() } } diff --git a/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt b/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt index a529e34bc1..2f4f524cc2 100644 --- a/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt +++ b/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStoreFactory class FakeUserPushStoreFactory : UserPushStoreFactory { - override fun create(userId: SessionId): UserPushStore { + override fun getOrCreate(userId: SessionId): UserPushStore { return FakeUserPushStore() } } diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 9851a8c7c2..b2c703fdb1 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -127,6 +127,7 @@ fun RoomSelectView( .consumeWindowInsets(paddingValues) ) { SearchBar( + modifier = Modifier.fillMaxWidth(), placeHolderTitle = stringResource(CommonStrings.action_search), query = state.query, onQueryChange = { state.eventSink(RoomSelectEvents.UpdateQuery(it)) }, diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index 7189442716..dffc1d1bbb 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -18,16 +18,32 @@ package io.element.android.libraries.sessionstorage.api import java.util.Date +/** + * Data class representing the session data to store locally. + */ data class SessionData( + /** The user ID of the logged in user. */ val userId: String, + /** The device ID of the session. */ val deviceId: String, + /** The current access token of the session. */ val accessToken: String, + /** The optional current refresh token of the session. */ val refreshToken: String?, + /** The homeserver URL of the session. */ val homeserverUrl: String, + /** The Open ID Connect info for this session, if any. */ val oidcData: String?, + /** The Sliding Sync Proxy URL for this session, if any. */ val slidingSyncProxy: String?, + /** The timestamp of the last login. May be `null` in very old sessions. */ val loginTimestamp: Date?, + /** Whether the [accessToken] is valid or not. */ val isTokenValid: Boolean, + /** The login type used to authenticate the session. */ val loginType: LoginType, + /** The optional passphrase used to encrypt data in the SDK local store. */ val passphrase: String?, + /** Whether the session needs verification. */ + val needsVerification: Boolean, ) diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index 3824def48c..6b79e867d0 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -34,6 +34,7 @@ internal fun SessionData.toDbModel(): DbSessionData { isTokenValid = if (isTokenValid) 1L else 0L, loginType = loginType.name, passphrase = passphrase, + needsVerification = if (needsVerification) 1L else 0L, ) } @@ -50,5 +51,6 @@ internal fun DbSessionData.toApiModel(): SessionData { isTokenValid = isTokenValid == 1L, loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), passphrase = passphrase, + needsVerification = needsVerification == 1L, ) } diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/6.db b/libraries/session-storage/impl/src/main/sqldelight/databases/6.db new file mode 100644 index 0000000000..8bf785b442 Binary files /dev/null and b/libraries/session-storage/impl/src/main/sqldelight/databases/6.db differ diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index c33b4d7c7e..56b036ec96 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -23,7 +23,9 @@ CREATE TABLE SessionData ( isTokenValid INTEGER NOT NULL DEFAULT 1, loginType TEXT, -- added in version 5 - passphrase TEXT + passphrase TEXT, + -- added in version 6 + needsVerification INTEGER NOT NULL DEFAULT 0 ); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/5.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/5.sqm new file mode 100644 index 0000000000..22797f1049 --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/5.sqm @@ -0,0 +1,4 @@ +-- Migrate DB from version 5 +-- For users migrating previously logged in sessions, we force them to verify them too + +ALTER TABLE SessionData ADD COLUMN needsVerification INTEGER NOT NULL DEFAULT 1; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt index 46e90f6d52..df35ec5944 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt @@ -144,6 +144,7 @@ class DatabaseSessionStoreTests { isTokenValid = 1, loginType = null, passphrase = "aPassphrase", + needsVerification = 1L, ) val secondSessionData = SessionData( userId = "userId", @@ -157,6 +158,7 @@ class DatabaseSessionStoreTests { isTokenValid = 1, loginType = null, passphrase = "aPassphraseAltered", + needsVerification = 0L, ) assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId) assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp) @@ -177,6 +179,7 @@ class DatabaseSessionStoreTests { assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp) assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData) assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase) + assertThat(alteredSession.needsVerification).isEqualTo(secondSessionData.needsVerification) } @Test @@ -193,6 +196,7 @@ class DatabaseSessionStoreTests { isTokenValid = 1, loginType = null, passphrase = "aPassphrase", + needsVerification = 1L, ) val secondSessionData = SessionData( userId = "userIdUnknown", @@ -206,6 +210,7 @@ class DatabaseSessionStoreTests { isTokenValid = 1, loginType = null, passphrase = "aPassphraseAltered", + needsVerification = 0L, ) assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId) @@ -224,5 +229,6 @@ class DatabaseSessionStoreTests { assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp) assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData) assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase) + assertThat(notAlteredSession.needsVerification).isEqualTo(firstSessionData.needsVerification) } } diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt index 341e5e0e92..1397e260b9 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt @@ -31,4 +31,5 @@ internal fun aSessionData() = SessionData( isTokenValid = 1, loginType = LoginType.UNKNOWN.name, passphrase = null, + needsVerification = 0L, ) diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index 1d237979e6..4374d77e52 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -100,4 +100,9 @@ object TestTags { * Timeline item. */ val timelineItemSenderInfo = TestTag("timeline_item-sender_info") + + /** + * Search field. + */ + val searchTextField = TestTag("search_text_field") } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 91c8bd9666..caa5e9f96b 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -63,6 +63,8 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo @@ -98,6 +100,7 @@ import kotlin.time.Duration.Companion.seconds fun TextComposer( state: RichTextEditorState, voiceMessageState: VoiceMessageState, + permalinkParser: PermalinkParser, composerMode: MessageComposerMode, enableTextFormatting: Boolean, enableVoiceMessages: Boolean, @@ -152,7 +155,10 @@ fun TextComposer( val textInput: @Composable () -> Unit = remember(state, subcomposing, composerMode, onResetComposerMode, onError) { @Composable { - val mentionSpanProvider = rememberMentionSpanProvider(currentUserId) + val mentionSpanProvider = rememberMentionSpanProvider( + currentUserId = currentUserId, + permalinkParser = permalinkParser, + ) TextInput( state = state, subcomposing = subcomposing, @@ -907,6 +913,10 @@ private fun ATextComposer( state = richTextEditorState, showTextFormatting = showTextFormatting, voiceMessageState = voiceMessageState, + permalinkParser = object : PermalinkParser { + override fun parse(uriString: String): PermalinkData = TODO("Not yet implemented") + override fun parse(uri: Uri): PermalinkData = TODO("Not yet implemented") + }, composerMode = composerMode, enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt index f7b28d65e8..e50b0fc16a 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.textcomposer.mentions import android.graphics.Color import android.graphics.Typeface +import android.net.Uri import android.view.ViewGroup import android.widget.TextView import androidx.compose.foundation.layout.PaddingValues @@ -42,10 +43,12 @@ import io.element.android.libraries.designsystem.theme.mentionPillText import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import kotlinx.collections.immutable.persistentListOf @Stable class MentionSpanProvider( private val currentSessionId: SessionId, + private val permalinkParser: PermalinkParser, private var currentUserTextColor: Int = 0, private var currentUserBackgroundColor: Int = Color.WHITE, private var otherTextColor: Int = 0, @@ -73,7 +76,7 @@ class MentionSpanProvider( } fun getMentionSpanFor(text: String, url: String): MentionSpan { - val permalinkData = PermalinkParser.parse(url) + val permalinkData = permalinkParser.parse(url) val (startPaddingPx, endPaddingPx) = paddingValuesPx.value return when { permalinkData is PermalinkData.UserLink -> { @@ -112,9 +115,15 @@ class MentionSpanProvider( } @Composable -fun rememberMentionSpanProvider(currentUserId: SessionId): MentionSpanProvider { +fun rememberMentionSpanProvider( + currentUserId: SessionId, + permalinkParser: PermalinkParser, +): MentionSpanProvider { val provider = remember(currentUserId) { - MentionSpanProvider(currentUserId) + MentionSpanProvider( + currentSessionId = currentUserId, + permalinkParser = permalinkParser, + ) } provider.setup() return provider @@ -123,7 +132,26 @@ fun rememberMentionSpanProvider(currentUserId: SessionId): MentionSpanProvider { @PreviewsDayNight @Composable internal fun MentionSpanPreview() { - val provider = rememberMentionSpanProvider(SessionId("@me:matrix.org")) + val provider = rememberMentionSpanProvider( + currentUserId = SessionId("@me:matrix.org"), + permalinkParser = object : PermalinkParser { + override fun parse(uriString: String): PermalinkData { + return when (uriString) { + "https://matrix.to/#/@me:matrix.org" -> PermalinkData.UserLink("@me:matrix.org") + "https://matrix.to/#/@other:matrix.org" -> PermalinkData.UserLink("@other:matrix.org") + "https://matrix.to/#/#room:matrix.org" -> PermalinkData.RoomLink( + roomIdOrAlias = "#room:matrix.org", + isRoomAlias = true, + eventId = null, + viaParameters = persistentListOf(), + ) + else -> TODO() + } + } + + override fun parse(uri: Uri): PermalinkData = TODO() + }, + ) ElementPreview { provider.setup() diff --git a/libraries/textcomposer/impl/src/main/res/values-be/translations.xml b/libraries/textcomposer/impl/src/main/res/values-be/translations.xml index 5da9e78476..7f5693b0a4 100644 --- a/libraries/textcomposer/impl/src/main/res/values-be/translations.xml +++ b/libraries/textcomposer/impl/src/main/res/values-be/translations.xml @@ -5,7 +5,7 @@ "Закрыць параметры фарматавання" "Пераключыць блок кода" "Паведамленне…" - "Стварыце спасылку" + "Стварыць спасылку" "Рэдагаваць спасылку" "Ужыць тоўсты шрыфт" "Ужыць курсіўны фармат" diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderTest.kt index c8281cca0e..475b879895 100644 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderTest.kt +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderTest.kt @@ -18,9 +18,12 @@ package io.element.android.libraries.textcomposer.impl.mentions import android.graphics.Color import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -35,8 +38,10 @@ class MentionSpanProviderTest { private val otherColor = Color.BLUE private val currentUserId = A_SESSION_ID + private val permalinkParser = FakePermalinkParser() private val mentionSpanProvider = MentionSpanProvider( currentSessionId = currentUserId, + permalinkParser = permalinkParser, currentUserBackgroundColor = myUserColor, currentUserTextColor = myUserColor, otherBackgroundColor = otherColor, @@ -45,6 +50,7 @@ class MentionSpanProviderTest { @Test fun `getting mention span for current user should return a MentionSpan with custom colors`() { + permalinkParser.givenResult(PermalinkData.UserLink(currentUserId.value)) val mentionSpan = mentionSpanProvider.getMentionSpanFor("@me:matrix.org", "https://matrix.to/#/${currentUserId.value}") assertThat(mentionSpan.backgroundColor).isEqualTo(myUserColor) assertThat(mentionSpan.textColor).isEqualTo(myUserColor) @@ -52,6 +58,7 @@ class MentionSpanProviderTest { @Test fun `getting mention span for other user should return a MentionSpan with normal colors`() { + permalinkParser.givenResult(PermalinkData.UserLink("@other:matrix.org")) val mentionSpan = mentionSpanProvider.getMentionSpanFor("@other:matrix.org", "https://matrix.to/#/@other:matrix.org") assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor) assertThat(mentionSpan.textColor).isEqualTo(otherColor) @@ -59,6 +66,14 @@ class MentionSpanProviderTest { @Test fun `getting mention span for a room should return a MentionSpan with normal colors`() { + permalinkParser.givenResult( + PermalinkData.RoomLink( + roomIdOrAlias = "#room:matrix.org", + isRoomAlias = true, + eventId = null, + viaParameters = persistentListOf(), + ) + ) val mentionSpan = mentionSpanProvider.getMentionSpanFor("#room:matrix.org", "https://matrix.to/#/#room:matrix.org") assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor) assertThat(mentionSpan.textColor).isEqualTo(otherColor) @@ -66,6 +81,14 @@ class MentionSpanProviderTest { @Test fun `getting mention span for @room should return a MentionSpan with normal colors`() { + permalinkParser.givenResult( + PermalinkData.RoomLink( + roomIdOrAlias = "#", + isRoomAlias = true, + eventId = null, + viaParameters = persistentListOf(), + ) + ) val mentionSpan = mentionSpanProvider.getMentionSpanFor("@room", "#") assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor) assertThat(mentionSpan.textColor).isEqualTo(otherColor) diff --git a/libraries/troubleshoot/api/build.gradle.kts b/libraries/troubleshoot/api/build.gradle.kts new file mode 100644 index 0000000000..5ac917fd0b --- /dev/null +++ b/libraries/troubleshoot/api/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.troubleshoot.api" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(libs.androidx.corektx) + implementation(libs.coroutines.core) +} diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/NotificationTroubleShootEntryPoint.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/NotificationTroubleShootEntryPoint.kt new file mode 100644 index 0000000000..6e4e1c39e3 --- /dev/null +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/NotificationTroubleShootEntryPoint.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint + +interface NotificationTroubleShootEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onDone() + } +} diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt new file mode 100644 index 0000000000..69073925fb --- /dev/null +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.api.test + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow + +interface NotificationTroubleshootTest { + val order: Int + val state: StateFlow + fun isRelevant(data: TestFilterData): Boolean = true + suspend fun run(coroutineScope: CoroutineScope) + suspend fun reset() + suspend fun quickFix(coroutineScope: CoroutineScope) { + error("Quick fix not implemented, you need to override this method in your test") + } +} diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestDelegate.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestDelegate.kt new file mode 100644 index 0000000000..da36de8ee7 --- /dev/null +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestDelegate.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.api.test + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * A NotificationTroubleshootTest delegate, with common pattern for running and resetting. + */ +class NotificationTroubleshootTestDelegate( + private val defaultName: String, + private val defaultDescription: String, + private val visibleWhenIdle: Boolean = true, + private val hasQuickFix: Boolean = false, + private val fakeDelay: Long = 0L, +) { + private val _state: MutableStateFlow = MutableStateFlow( + NotificationTroubleshootTestState( + name = defaultName, + description = defaultDescription, + status = NotificationTroubleshootTestState.Status.Idle(visibleWhenIdle), + ) + ) + + val state: StateFlow = _state.asStateFlow() + + suspend fun updateState( + status: NotificationTroubleshootTestState.Status, + name: String = defaultName, + description: String = defaultDescription, + ) { + _state.emit( + NotificationTroubleshootTestState( + name = name, + description = description, + status = status, + ) + ) + } + + suspend fun reset() { + updateState(NotificationTroubleshootTestState.Status.Idle(visibleWhenIdle)) + } + + suspend fun start() { + updateState(NotificationTroubleshootTestState.Status.InProgress) + delay(fakeDelay) + } + + suspend fun done(isSuccess: Boolean = true) { + updateState( + if (isSuccess) { + NotificationTroubleshootTestState.Status.Success + } else { + NotificationTroubleshootTestState.Status.Failure(hasQuickFix) + } + ) + } + + companion object { + const val SHORT_DELAY = 300L + const val LONG_DELAY = 500L + } +} diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestState.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestState.kt new file mode 100644 index 0000000000..1429916bcc --- /dev/null +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTestState.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.api.test + +data class NotificationTroubleshootTestState( + val name: String, + val description: String, + val status: Status, +) { + sealed interface Status { + data class Idle(val visible: Boolean) : Status + data object InProgress : Status + data object WaitingForUser : Status + data object Success : Status + data class Failure(val hasQuickFix: Boolean) : Status + } +} diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/TestFilterData.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/TestFilterData.kt new file mode 100644 index 0000000000..77f633363d --- /dev/null +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/TestFilterData.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.api.test + +data class TestFilterData( + val currentPushProviderName: String?, +) diff --git a/libraries/troubleshoot/impl/build.gradle.kts b/libraries/troubleshoot/impl/build.gradle.kts new file mode 100644 index 0000000000..4967a34cf3 --- /dev/null +++ b/libraries/troubleshoot/impl/build.gradle.kts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "io.element.android.libraries.troubleshoot.impl" + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + implementation(libs.dagger) + implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.di) + api(projects.libraries.troubleshoot.api) + api(projects.libraries.push.api) + implementation(projects.services.analytics.api) + ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.test.robolectric) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.coroutines.test) + testImplementation(projects.services.analytics.test) + testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.push.test) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPoint.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPoint.kt new file mode 100644 index 0000000000..81c0cdaf00 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/DefaultNotificationTroubleShootEntryPoint.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultNotificationTroubleShootEntryPoint @Inject constructor() : NotificationTroubleShootEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NotificationTroubleShootEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : NotificationTroubleShootEntryPoint.NodeBuilder { + override fun callback(callback: NotificationTroubleShootEntryPoint.Callback): NotificationTroubleShootEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvents.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvents.kt new file mode 100644 index 0000000000..f2398e3b78 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +sealed interface TroubleshootNotificationsEvents { + data object StartTests : TroubleshootNotificationsEvents + data object RetryFailedTests : TroubleshootNotificationsEvents + data class QuickFix(val testIndex: Int) : TroubleshootNotificationsEvents +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsNode.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsNode.kt new file mode 100644 index 0000000000..96404d6d8c --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsNode.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +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 im.vector.app.features.analytics.plan.MobileScreen +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.troubleshoot.api.NotificationTroubleShootEntryPoint +import io.element.android.services.analytics.api.ScreenTracker + +@ContributesNode(SessionScope::class) +class TroubleshootNotificationsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: TroubleshootNotificationsPresenter, + private val screenTracker: ScreenTracker, +) : Node(buildContext, plugins = plugins) { + private fun onDone() { + plugins().forEach { + it.onDone() + } + } + + @Composable + override fun View(modifier: Modifier) { + screenTracker.TrackScreen(MobileScreen.ScreenName.NotificationTroubleshoot) + val state = presenter.present() + TroubleshootNotificationsView( + state = state, + onBackPressed = ::onDone, + modifier = modifier, + ) + } +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt new file mode 100644 index 0000000000..a730199fac --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.launch +import javax.inject.Inject + +class TroubleshootNotificationsPresenter @Inject constructor( + private val troubleshootTestSuite: TroubleshootTestSuite, +) : Presenter { + @Composable + override fun present(): TroubleshootNotificationsState { + val coroutineScope = rememberCoroutineScope() + LaunchedEffect(Unit) { + troubleshootTestSuite.start(this) + } + + val testSuiteState by troubleshootTestSuite.state.collectAsState() + fun handleEvents(event: TroubleshootNotificationsEvents) { + when (event) { + TroubleshootNotificationsEvents.StartTests -> coroutineScope.launch { + troubleshootTestSuite.runTestSuite(this) + } + is TroubleshootNotificationsEvents.QuickFix -> coroutineScope.launch { + troubleshootTestSuite.quickFix(event.testIndex, this) + } + TroubleshootNotificationsEvents.RetryFailedTests -> coroutineScope.launch { + troubleshootTestSuite.retryFailedTest(this) + } + } + } + + return TroubleshootNotificationsState( + testSuiteState = testSuiteState, + eventSink = ::handleEvents + ) + } +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsState.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsState.kt new file mode 100644 index 0000000000..4ff7c2c04c --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +data class TroubleshootNotificationsState( + val testSuiteState: TroubleshootTestSuiteState, + val eventSink: (TroubleshootNotificationsEvents) -> Unit, +) { + val hasFailedTests: Boolean = testSuiteState.mainState.isFailure() +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt new file mode 100644 index 0000000000..f2d49a82a0 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import kotlinx.collections.immutable.toImmutableList + +open class TroubleshootNotificationsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aTroubleshootNotificationsState( + listOf( + aTroubleshootTestStateIdle(), + aTroubleshootTestStateIdle(), + aTroubleshootTestStateIdle(visible = false), + ) + ), + aTroubleshootNotificationsState( + listOf( + aTroubleshootTestStateInProgress(), + aTroubleshootTestStateIdle(), + aTroubleshootTestStateIdle(), + ) + ), + aTroubleshootNotificationsState( + listOf( + aTroubleshootTestStateSuccess(), + aTroubleshootTestStateInProgress(), + aTroubleshootTestStateIdle(), + ) + ), + aTroubleshootNotificationsState( + listOf( + aTroubleshootTestStateSuccess(), + aTroubleshootTestStateWaitingForUser(), + aTroubleshootTestStateIdle(), + ) + ), + aTroubleshootNotificationsState( + listOf( + aTroubleshootTestStateSuccess(), + aTroubleshootTestStateFailure(hasQuickFix = true), + aTroubleshootTestStateInProgress(), + ) + ), + aTroubleshootNotificationsState( + listOf( + aTroubleshootTestStateSuccess(), + aTroubleshootTestStateFailure(hasQuickFix = true), + aTroubleshootTestStateFailure(hasQuickFix = false), + ) + ), + aTroubleshootNotificationsState( + listOf( + aTroubleshootTestStateSuccess(), + aTroubleshootTestStateSuccess(), + aTroubleshootTestStateSuccess(), + ) + ), + aTroubleshootNotificationsState( + listOf( + aTroubleshootTestStateWaitingForUser(), + ) + ), + ) +} + +fun aTroubleshootNotificationsState( + tests: List = emptyList(), + eventSink: (TroubleshootNotificationsEvents) -> Unit = {}, +) = TroubleshootNotificationsState( + eventSink = eventSink, + testSuiteState = TroubleshootTestSuiteState( + mainState = tests.computeMainState(), + tests = tests.toImmutableList(), + ), +) + +fun aTroubleshootTestState( + status: NotificationTroubleshootTestState.Status, + name: String = "Test", + description: String = "Description", +): NotificationTroubleshootTestState { + return NotificationTroubleshootTestState( + name = name, + description = description, + status = status, + ) +} + +fun aTroubleshootTestStateIdle(visible: Boolean = true) = + aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.Idle(visible = visible)) + +fun aTroubleshootTestStateInProgress() = + aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.InProgress) + +fun aTroubleshootTestStateWaitingForUser() = + aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.WaitingForUser) + +fun aTroubleshootTestStateSuccess() = + aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.Success) + +fun aTroubleshootTestStateFailure(hasQuickFix: Boolean) = + aTroubleshootTestState(status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = hasQuickFix)) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt new file mode 100644 index 0000000000..8b2ba843be --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics +import androidx.compose.runtime.Composable +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.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.components.preferences.PreferencePage +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.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState.Status + +@Composable +fun TroubleshootNotificationsView( + state: TroubleshootNotificationsState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + if (state.hasFailedTests) { + state.eventSink(TroubleshootNotificationsEvents.RetryFailedTests) + } + } + else -> Unit + } + } + + PreferencePage( + modifier = modifier, + onBackPressed = onBackPressed, + title = stringResource(id = R.string.troubleshoot_notifications_screen_title), + ) { + TroubleshootNotificationsContent(state) + } +} + +@Composable +private fun TroubleshootTestView( + testState: NotificationTroubleshootTestState, + onQuickFixClicked: () -> Unit, +) { + if ((testState.status as? Status.Idle)?.visible == false) return + ListItem( + headlineContent = { Text(text = testState.name) }, + supportingContent = { Text(text = testState.description) }, + trailingContent = when (testState.status) { + is Status.Idle -> null + Status.InProgress -> ListItemContent.Custom { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .size(20.dp), + strokeWidth = 2.dp + ) + } + Status.WaitingForUser -> ListItemContent.Custom { + Icon( + contentDescription = null, + modifier = Modifier.size(24.dp), + imageVector = CompoundIcons.Info(), + tint = ElementTheme.colors.iconAccentTertiary + ) + } + Status.Success -> ListItemContent.Custom { + Icon( + contentDescription = null, + modifier = Modifier.size(24.dp), + imageVector = CompoundIcons.Check(), + tint = ElementTheme.colors.iconAccentTertiary + ) + } + is Status.Failure -> ListItemContent.Custom { + Icon( + contentDescription = null, + modifier = Modifier.size(24.dp), + imageVector = CompoundIcons.Error(), + tint = ElementTheme.colors.textCriticalPrimary + ) + } + } + ) + if ((testState.status as? Status.Failure)?.hasQuickFix == true) { + ListItem( + headlineContent = { + }, + trailingContent = ListItemContent.Custom { + Button( + text = stringResource(id = R.string.troubleshoot_notifications_screen_quick_fix_action), + onClick = onQuickFixClicked + ) + } + ) + } +} + +@Composable +private fun TroubleshootNotificationsContent(state: TroubleshootNotificationsState) { + when (state.testSuiteState.mainState) { + AsyncAction.Loading, + AsyncAction.Confirming, + is AsyncAction.Success, + is AsyncAction.Failure -> { + TestSuiteView( + testSuiteState = state.testSuiteState, + onQuickFixClicked = { + state.eventSink(TroubleshootNotificationsEvents.QuickFix(it)) + } + ) + } + AsyncAction.Uninitialized -> Unit + } + when (state.testSuiteState.mainState) { + AsyncAction.Uninitialized -> { + ListItem(headlineContent = { + Text( + text = stringResource(id = R.string.troubleshoot_notifications_screen_notice) + ) + }) + RunTestButton(state = state) + } + AsyncAction.Loading -> Unit + is AsyncAction.Failure -> { + ListItem(headlineContent = { + Text(text = stringResource(id = R.string.troubleshoot_notifications_screen_failure)) + }) + RunTestButton(state = state) + } + AsyncAction.Confirming -> { + ListItem(headlineContent = { + Text( + text = stringResource(id = R.string.troubleshoot_notifications_screen_waiting) + ) + }) + } + is AsyncAction.Success -> { + ListItem(headlineContent = { + Text( + text = stringResource(id = R.string.troubleshoot_notifications_screen_success) + ) + }) + } + } +} + +@Composable +private fun RunTestButton(state: TroubleshootNotificationsState) { + ListItem( + headlineContent = { + Button( + text = stringResource( + id = if (state.testSuiteState.mainState is AsyncAction.Failure) { + R.string.troubleshoot_notifications_screen_action_again + } else { + R.string.troubleshoot_notifications_screen_action + } + ), + onClick = { + state.eventSink(TroubleshootNotificationsEvents.StartTests) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + ) +} + +@Composable +private fun TestSuiteView( + testSuiteState: TroubleshootTestSuiteState, + onQuickFixClicked: (Int) -> Unit, +) { + testSuiteState.tests.forEachIndexed { index, testState -> + TroubleshootTestView( + testState = testState, + onQuickFixClicked = { + onQuickFixClicked(index) + }, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun TroubleshootNotificationsViewPreview( + @PreviewParameter(TroubleshootNotificationsStateProvider::class) state: TroubleshootNotificationsState, +) = ElementPreview { + TroubleshootNotificationsView( + state = state, + onBackPressed = {}, + ) +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuite.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuite.kt new file mode 100644 index 0000000000..1c5fbbc3e3 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuite.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +import im.vector.app.features.analytics.plan.NotificationTroubleshoot +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.push.api.GetCurrentPushProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class TroubleshootTestSuite @Inject constructor( + private val notificationTroubleshootTests: Set<@JvmSuppressWildcards NotificationTroubleshootTest>, + private val getCurrentPushProvider: GetCurrentPushProvider, + private val analyticsService: AnalyticsService, +) { + lateinit var tests: List + + private val _state: MutableStateFlow = MutableStateFlow( + TroubleshootTestSuiteState( + mainState = AsyncAction.Uninitialized, + tests = emptyList().toImmutableList() + ) + ) + val state: StateFlow = _state + + suspend fun start(coroutineScope: CoroutineScope) { + val testFilterData = TestFilterData( + currentPushProviderName = getCurrentPushProvider.getCurrentPushProvider() + ) + tests = notificationTroubleshootTests + .filter { it.isRelevant(testFilterData) } + .sortedBy { it.order } + tests.forEach { + // Observe the state of the tests + it.state.onEach { + emitState() + }.launchIn(coroutineScope) + } + } + + suspend fun runTestSuite(coroutineScope: CoroutineScope) { + tests.forEach { + it.reset() + } + tests.forEach { + it.run(coroutineScope) + } + } + + suspend fun retryFailedTest(coroutineScope: CoroutineScope) { + tests + .filter { it.state.value.status is NotificationTroubleshootTestState.Status.Failure } + .forEach { + it.run(coroutineScope) + } + } + + private suspend fun emitState() { + val states = tests.map { it.state.value } + val mainState = states.computeMainState() + when (mainState) { + is AsyncAction.Success -> { + analyticsService.capture(NotificationTroubleshoot(hasError = false)) + } + is AsyncAction.Failure -> { + analyticsService.capture(NotificationTroubleshoot(hasError = true)) + } + else -> Unit + } + _state.emit( + TroubleshootTestSuiteState( + mainState = states.computeMainState(), + tests = states.toImmutableList() + ) + ) + } + + suspend fun quickFix(testIndex: Int, coroutineScope: CoroutineScope) { + tests[testIndex].quickFix(coroutineScope) + } +} + +fun List.computeMainState(): AsyncAction { + val isIdle = all { it.status is NotificationTroubleshootTestState.Status.Idle } + val isRunning = any { it.status is NotificationTroubleshootTestState.Status.InProgress } + return when { + isIdle -> AsyncAction.Uninitialized + isRunning -> AsyncAction.Loading + else -> { + if (any { it.status is NotificationTroubleshootTestState.Status.WaitingForUser }) { + AsyncAction.Confirming + } else if (any { it.status is NotificationTroubleshootTestState.Status.Failure }) { + AsyncAction.Failure(Exception("Some tests failed")) + } else { + AsyncAction.Success(Unit) + } + } + } +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuiteState.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuiteState.kt new file mode 100644 index 0000000000..32e71ff9a5 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootTestSuiteState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import kotlinx.collections.immutable.ImmutableList + +data class TroubleshootTestSuiteState( + val mainState: AsyncAction, + val tests: ImmutableList, +) diff --git a/libraries/troubleshoot/impl/src/main/res/values-be/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-be/translations.xml new file mode 100644 index 0000000000..905b9e89b6 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-be/translations.xml @@ -0,0 +1,11 @@ + + + "Запусціць тэсты" + "Запусціце тэсты яшчэ раз" + "Некаторыя тэсты не ўдаліся. Калі ласка, праглядзіце дэталі." + "Запусціце тэсты, каб выявіць праблемы ў вашай канфігурацыі, з-за якіх апавяшчэння могуць паводзіць сябе не так, як чакалася." + "Спроба выпраўлення" + "Усе тэсты паспяхова пройдзены." + "Выпраўленне непаладак з апавяшчэннямі" + "Некаторыя тэсты патрабуюць вашай увагі. Калі ласка, праглядзіце дэталі." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-cs/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..17e47cf56f --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,11 @@ + + + "Spustit testy" + "Spustit testy znovu" + "Některé testy selhaly. Zkontrolujte prosím podrobnosti." + "Spusťte testy, abyste zjistili jakýkoli problém ve vaší konfiguraci, který může způsobit, že se oznámení nebudou chovat podle očekávání." + "Pokus o opravu" + "Všechny testy proběhly úspěšně." + "Odstraňování problémů s upozorněními" + "Některé testy vyžadují vaši pozornost. Zkontrolujte prosím podrobnosti." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..98b8c579a9 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,11 @@ + + + "Tests durchführen" + "Tests erneut durchführen" + "Einige Tests sind fehlgeschlagen. Bitte überprüfe die Details." + "Führe die Tests durch, um Probleme zu erkennen, die dazu führen können, dass sich die Benachrichtigungen nicht wie erwartet verhalten." + "Versuche das Problem zu beheben" + "Alle Tests wurden erfolgreich bestanden." + "Fehlerbehebung für Benachrichtigungen" + "Einige Tests erfordern deine Aufmerksamkeit. Bitte überprüfe die Details." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-fr/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..497b3e3b56 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,11 @@ + + + "Exécuter les tests" + "Relancer les tests" + "Certains tests ont échoué. Veuillez vérifier les détails." + "Exécuter les tests pour détecter tout problème dans votre configuration susceptible de provoquer un dysfonctionnement des notifications." + "Tenter de corriger" + "Tous les tests ont réussi." + "Dépanner les notifications" + "Certains tests nécessitent votre attention. Veuillez vérifier les détails." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml new file mode 100644 index 0000000000..211b1f4a1d --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-hu/translations.xml @@ -0,0 +1,11 @@ + + + "Tesztek futtatása" + "Tesztek újbóli futtatása" + "Egyes tesztek sikertelenek voltak. Ellenőrizze a részleteket." + "A tesztek futtatása, hogy észlelje a konfigurációban felmerülő olyan problémákat, amelyek miatt az értesítések nem az elvárt módon viselkednek." + "Kísérlet a javításra" + "Minden teszt sikeresen lezajlott." + "Értesítések hibaelhárítása" + "Egyes tesztek a figyelmét igénylik. Ellenőrizze a részleteket." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..943652fc86 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,11 @@ + + + "Выполнение тестов" + "Повторное выполнение тестов" + "Некоторые тесты провалились. Пожалуйста, проверьте детали." + "Выполните тесты, чтобы обнаружить любую проблему в конфигурации, из-за которой уведомления могут работать не так, как ожидалось." + "Попытка исправить" + "Все тесты прошли успешно." + "Уведомления об устранении неполадок" + "Некоторые тесты требуют вашего внимания. Пожалуйста, проверьте детали." + diff --git a/libraries/troubleshoot/impl/src/main/res/values-sk/translations.xml b/libraries/troubleshoot/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..3b7a08c2f0 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,11 @@ + + + "Spustiť testy" + "Spustiť testy znova" + "Niektoré testy zlyhali. Skontrolujte prosím podrobnosti." + "Spustite testy, aby ste zistili akýkoľvek problém vo vašej konfigurácii, ktorý môže spôsobiť, že sa upozornenia nebudú správať podľa očakávania." + "Pokus o opravu" + "Všetky testy prebehli úspešne." + "Oznámenia riešení problémov" + "Niektoré testy si vyžadujú vašu pozornosť. Prosím skontrolujte podrobnosti." + diff --git a/libraries/troubleshoot/impl/src/main/res/values/localazy.xml b/libraries/troubleshoot/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..eee100d711 --- /dev/null +++ b/libraries/troubleshoot/impl/src/main/res/values/localazy.xml @@ -0,0 +1,11 @@ + + + "Run tests" + "Run tests again" + "Some tests failed. Please check the details." + "Run the tests to detect any issue in your configuration that may make notifications not behave as expected." + "Attempt to fix" + "All tests passed successfully." + "Troubleshoot notifications" + "Some tests require your attention. Please check the details." + diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/FakeNotificationTroubleshootTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/FakeNotificationTroubleshootTest.kt new file mode 100644 index 0000000000..90b8a988bb --- /dev/null +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/FakeNotificationTroubleshootTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeNotificationTroubleshootTest( + override val order: Int = 0, + private val defaultName: String = "test name", + private val defaultDescription: String = "test description", + private val firstStatus: NotificationTroubleshootTestState.Status = NotificationTroubleshootTestState.Status.Idle(visible = true), + private val runAction: () -> NotificationTroubleshootTestState? = { null }, + private val resetAction: () -> NotificationTroubleshootTestState? = { null }, + private val quickFixAction: () -> NotificationTroubleshootTestState? = { null }, +) : NotificationTroubleshootTest { + private val _state = MutableStateFlow( + NotificationTroubleshootTestState( + name = defaultName, + description = defaultDescription, + status = firstStatus + ) + ) + override val state: StateFlow = _state.asStateFlow() + + override suspend fun run(coroutineScope: CoroutineScope) { + updateState(NotificationTroubleshootTestState.Status.InProgress) + runAction()?.let { + _state.tryEmit(it) + } + } + + override suspend fun reset() { + updateState( + name = defaultName, + description = defaultDescription, + status = firstStatus, + ) + resetAction()?.let { + _state.emit(it) + } + } + + override suspend fun quickFix(coroutineScope: CoroutineScope) { + updateState(NotificationTroubleshootTestState.Status.InProgress) + quickFixAction()?.let { + _state.emit(it) + } + } + + suspend fun updateState( + status: NotificationTroubleshootTestState.Status, + name: String = defaultName, + description: String = defaultDescription, + ) { + _state.emit( + NotificationTroubleshootTestState( + name = name, + description = description, + status = status, + ) + ) + } +} diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTests.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTests.kt new file mode 100644 index 0000000000..25082b63c4 --- /dev/null +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTests.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +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.libraries.architecture.AsyncAction +import io.element.android.libraries.push.test.FakeGetCurrentPushProvider +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest +import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.services.analytics.test.FakeAnalyticsService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class TroubleshootNotificationsPresenterTests { + @Test + fun `present - initial state`() = runTest { + val presenter = createTroubleshootNotificationsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.testSuiteState.tests).isEmpty() + assertThat(initialState.testSuiteState.mainState).isEqualTo(AsyncAction.Uninitialized) + } + } + + @Test + fun `present - start test`() = runTest { + val troubleshootTestSuite = createTroubleshootTestSuite( + tests = setOf(FakeNotificationTroubleshootTest()) + ) + val presenter = createTroubleshootNotificationsPresenter( + troubleshootTestSuite = troubleshootTestSuite, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(TroubleshootNotificationsEvents.StartTests) + skipItems(1) + val stateAfterStart = awaitItem() + assertThat(stateAfterStart.testSuiteState.mainState).isEqualTo(AsyncAction.Loading) + } + } + + @Test + fun `present - start failed test`() = runTest { + val troubleshootTestSuite = createTroubleshootTestSuite( + tests = setOf( + FakeNotificationTroubleshootTest( + firstStatus = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = false) + ) + ) + ) + val presenter = createTroubleshootNotificationsPresenter( + troubleshootTestSuite = troubleshootTestSuite, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(TroubleshootNotificationsEvents.RetryFailedTests) + skipItems(1) + val stateAfterStart = awaitItem() + assertThat(stateAfterStart.testSuiteState.mainState).isEqualTo(AsyncAction.Loading) + } + } + + @Test + fun `present - quick fix test`() = runTest { + val troubleshootTestSuite = createTroubleshootTestSuite( + tests = setOf( + FakeNotificationTroubleshootTest( + firstStatus = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = false) + ) + ) + ) + val presenter = createTroubleshootNotificationsPresenter( + troubleshootTestSuite = troubleshootTestSuite, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.testSuiteState.mainState).isInstanceOf(AsyncAction.Failure::class.java) + initialState.eventSink(TroubleshootNotificationsEvents.QuickFix(0)) + val stateAfterStart = awaitItem() + assertThat(stateAfterStart.testSuiteState.mainState).isEqualTo(AsyncAction.Loading) + } + } + + private fun createTroubleshootTestSuite( + tests: Set = emptySet(), + currentPushProvider: String? = null, + ): TroubleshootTestSuite { + return TroubleshootTestSuite( + notificationTroubleshootTests = tests, + getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider), + analyticsService = FakeAnalyticsService(), + ) + } + + private fun createTroubleshootNotificationsPresenter( + troubleshootTestSuite: TroubleshootTestSuite = createTroubleshootTestSuite(), + ): TroubleshootNotificationsPresenter { + return TroubleshootNotificationsPresenter( + troubleshootTestSuite = troubleshootTestSuite, + ) + } +} diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt new file mode 100644 index 0000000000..5acd0e5635 --- /dev/null +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.troubleshoot.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class TroubleshootNotificationsViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `press menu back invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setTroubleshootNotificationsView( + state = aTroubleshootNotificationsState( + eventSink = eventsRecorder + ), + onBackPressed = it, + ) + rule.pressBack() + } + } + + @Test + fun `clicking on run test emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setTroubleshootNotificationsView( + aTroubleshootNotificationsState( + eventSink = eventsRecorder + ), + ) + rule.onNodeWithText("Run tests").performClick() + eventsRecorder.assertSingle(TroubleshootNotificationsEvents.StartTests) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on run test again emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setTroubleshootNotificationsView( + aTroubleshootNotificationsState( + tests = listOf( + aTroubleshootTestStateFailure( + hasQuickFix = false + ) + ), + eventSink = eventsRecorder + ), + ) + rule.onNodeWithText("Run tests again").performClick() + eventsRecorder.assertList( + listOf( + TroubleshootNotificationsEvents.RetryFailedTests, + TroubleshootNotificationsEvents.StartTests, + ) + ) + } + + @Config(qualifiers = "h1024dp") + @Test + fun `clicking on quick fix emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setTroubleshootNotificationsView( + aTroubleshootNotificationsState( + tests = listOf( + aTroubleshootTestStateFailure( + hasQuickFix = true + ) + ), + eventSink = eventsRecorder + ), + ) + rule.onNodeWithText("Attempt to fix").performClick() + eventsRecorder.assertList( + listOf( + TroubleshootNotificationsEvents.RetryFailedTests, + TroubleshootNotificationsEvents.QuickFix(0), + ) + ) + } +} + +private fun AndroidComposeTestRule.setTroubleshootNotificationsView( + state: TroubleshootNotificationsState, + onBackPressed: () -> Unit = EnsureNeverCalled(), +) { + setContent { + TroubleshootNotificationsView( + state = state, + onBackPressed = onBackPressed, + ) + } +} diff --git a/libraries/ui-strings/src/main/res/values-be/translations.xml b/libraries/ui-strings/src/main/res/values-be/translations.xml index f163de2540..659f71e754 100644 --- a/libraries/ui-strings/src/main/res/values-be/translations.xml +++ b/libraries/ui-strings/src/main/res/values-be/translations.xml @@ -34,13 +34,13 @@ "Запісаць галасавое паведамленне." "Спыніць запіс" "Прыняць" - "Дадаць на часовую шкалу" + "Дадаць у хроніку" "Назад" "Скасаваць" "Выбраць фота" "Ачысціць" "Закрыць" - "Поўная праверка" + "Праверка завершана" "Пацвердзіць" "Працягнуць" "Капіраваць" @@ -90,7 +90,7 @@ "Паскардзіцца на змест" "Скінуць" "Паўтарыць" - "Паўтарыце расшыфроўку" + "Паўтарыць расшыфроўку" "Захаваць" "Пошук" "Адправіць" @@ -108,7 +108,7 @@ "Зрабіць фота" "Дакраніцеся, каб убачыць параметры" "Паўтарыць спробу" - "Паказаць крыніцу" + "Прагляд зыходнага кода" "Так" "Аб праграме" "Палітыка дапушчальнага выкарыстання" @@ -119,7 +119,7 @@ "Заблакіраваныя карыстальнікі" "Бурбалкі" "Ідзе выклік (не падтрымліваецца)" - "Рэзервовае капіраванне чата" + "Рэзервовае капіраванне чатаў" "Аўтарскае права" "Стварэнне пакоя…" "Выйшаў з пакоя" @@ -150,9 +150,9 @@ "Спасылка скапіравана ў буфер абмену" "Загрузка…" - "%1$d карыстальнік" - "%1$d карыстальнікаў" - "%1$d карыстальнікаў" + "%1$d удзельнік" + "%1$d удзельніка" + "%1$d удзельнікаў" "Паведамленне" "Дзеянні з паведамленням" @@ -164,7 +164,7 @@ "Па-за сеткай" "або" "Пароль" - "Людзі" + "Удзельнікі" "Пастаянная спасылка" "Дазвол" "Вы ўпэўнены, што хочаце скончыць гэтае апытанне?" @@ -173,7 +173,7 @@ "Вынікі будуць паказаны пасля завяршэння апытання" "%d голас" - "%d галасоў" + "%d галасы" "%d галасоў" "Палітыка прыватнасці" @@ -202,7 +202,7 @@ "Сервер не падтрымліваецца" "URL-адрас сервера" "Налады" - "Абагуленыя геаданыя" + "Абагуленае месцазнаходжанне" "Выхад" "Пачатак чата…" "Стыкер" @@ -248,23 +248,16 @@ "🔐️ Далучайцеся да мяне %1$s" "Гэй, пагавары са мной у %1$s: %2$s" "%1$s Android" - "Rageshake паведаміць пра памылку" - "Пацвердзіце гэтую прыладу, каб наладзіць бяспечны абмен паведамленнямі." - "Пацвердзіце, што гэта вы" - "Цяпер вы можаце бяспечна чытаць і адпраўляць паведамленні, і ўсе, з кім вы маеце зносіны ў чаце, таксама могуць давяраць гэтай прыладзе." - "Прылада праверана" - "Выкарыстоўвайце іншую прыладу" - "Чакаем іншую прыладу…" + "Паведаміць аб памылцы з дапамогай Rageshake" "Не ўдалося выбраць носьбіт, паўтарыце спробу." "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не атрымалася загрузіць медыяфайлы, паспрабуйце яшчэ раз." - "Памылка загрузкі" - "Каталог пакояў" "Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз." "Не ўдалося атрымаць інфармацыю пра карыстальніка" "Заблакіраваць" "Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час." "Заблакіраваць карыстальніка" + "Профіль" "Разблакіраваць" "Вы зноў зможаце ўбачыць усе паведамленні." "Разблакіраваць карыстальніка" diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index 6975c64660..b92077b657 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -179,7 +179,7 @@ "Zásady ochrany osobních údajů" "Reakce" "Reakce" - "Klíč pro obnovu" + "Klíč pro obnovení" "Obnovování…" "Odpověď na %1$s" "Nahlásit chybu" @@ -249,22 +249,15 @@ "Ahoj, ozvi se mi na %1$s: %2$s" "%1$s Android" "Zatřeste zařízením pro nahlášení chyby" - "Ověřte toto zařízení a nastavte zabezpečené zasílání zpráv." - "Potvrďte, že jste to vy" - "Nyní můžete bezpečně číst nebo odesílat zprávy, a kdokoli, s kým chatujete, může tomuto zařízení důvěřovat." - "Zařízení ověřeno" - "Použít jiné zařízení" - "Čekání na jiném zařízení…" "Výběr média se nezdařil, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." - "Načítání se nezdařilo" - "Adresář místností" "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nepodařilo se načíst údaje o uživateli" "Zablokovat" "Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat." "Zablokovat uživatele" + "Profil" "Odblokovat" "Znovu uvidíte všechny zprávy od nich." "Odblokovat uživatele" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 83b3dcac0a..b7f24c80c6 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -175,7 +175,9 @@ "Datenschutz­erklärung" "Reaktion" "Reaktionen" - "Wiederherstellungsschlüssel" + + "Wiederherstellungsschlüssel" + "Wird erneuert…" "%1$s antworten" "Einen Fehler melden" @@ -245,22 +247,15 @@ "Hey, sprich mit mir auf %1$s: %2$s" "%1$s Android" "Schüttel heftig zum Melden von Fehlern" - "Verifiziere dieses Gerät, um sicheres Messaging einzurichten." - "Bestätige, dass du es bist" - "Du kannst nun verschlüsselte Nachrichten lesen oder versenden." - "Gerät verifiziert" - "Ein anderes Gerät verwenden" - "Bitte warten bis das andere Gerät bereit ist." "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut." - "Fehler beim Laden" - "Raumverzeichnis" "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." "Benutzerdetails konnten nicht abgerufen werden" "Blockieren" "Blockierte Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden." "Benutzer blockieren" + "Profil" "Blockierung aufheben" "Der Nutzer kann dir wieder Nachrichten senden & alle Nachrichten des Nutzers werden wieder angezeigt." "Blockierung aufheben" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 219b8501c5..1b9befbc45 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -245,17 +245,9 @@ "Salut, parle-moi sur %1$s : %2$s" "%1$s Android" "Rageshake pour signaler un problème" - "Vérifier cette session pour configurer votre messagerie sécurisée." - "Confirmez votre identité" - "Vous pouvez désormais lire ou envoyer des messages en toute sécurité, et toute personne avec qui vous discutez peut également faire confiance à cette session." - "Session vérifiée" - "Utiliser une autre session" - "En attente d’une autre session…" "Échec de la sélection du média, veuillez réessayer." "Échec du traitement des médias à télécharger, veuillez réessayer." "Échec du téléchargement du média, veuillez réessayer." - "Échec du chargement" - "Annuaire des salons" "Échec du traitement des médias à télécharger, veuillez réessayer." "Impossible de récupérer les détails de l’utilisateur" "Bloquer" diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml index 0d7886a21b..aed5aefacf 100644 --- a/libraries/ui-strings/src/main/res/values-hu/translations.xml +++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml @@ -55,8 +55,8 @@ "Szavazás szerkesztése" "Engedélyezés" "Szavazás lezárása" - "Add meg a PIN-kódot" - "Elfelejtetted a jelszavadat?" + "Adja meg a PIN-kódot" + "Elfelejtette a jelszót?" "Tovább" "Visszalépés" "Meghívás" @@ -95,17 +95,17 @@ "Üzenet küldése" "Megosztás" "Hivatkozás megosztása" - "Jelentkezz be újra" + "Jelentkezzen be újra" "Kijelentkezés" "Kijelentkezés mindenképp" "Kihagyás" "Indítás" "Csevegés indítása" "Ellenőrzés elindítása" - "Koppints a térkép betöltéséhez" + "Koppintson a térkép betöltéséhez" "Fénykép készítése" - "Koppints a beállításokért" - "Próbáld újra" + "Koppintson a beállításokért" + "Próbálja újra" "Forrás megtekintése" "Igen" "Névjegy" @@ -120,7 +120,7 @@ "Csevegés biztonsági mentése" "Szerzői jogok" "Szoba létrehozása…" - "Elhagytad a szobát" + "Elhagyta a szobát" "Sötét" "Visszafejtési hiba" "Fejlesztői beállítások" @@ -129,7 +129,7 @@ "Szerkesztés" "* %1$s %2$s" "Titkosítás engedélyezve" - "Add meg a PIN-kódodat" + "Adja meg a PIN-kódját" "Hiba" "Mindenki" "Sikertelen" @@ -184,7 +184,7 @@ "Formázott szöveges szerkesztő" "Szoba" "Szoba neve" - "például a projekted neve" + "például a projektje neve" "Mentett módosítások" "Mentés" "Képernyőzár" @@ -232,23 +232,19 @@ "A módosítások nem lettek mentve. Biztos, hogy visszalép?" "Menti a módosításokat?" "Nem sikerült létrehozni az állandó hivatkozást" - "A(z) %1$s nem tudta betölteni a térképet. Próbáld meg újra később." + "Az %1$s nem tudta betölteni a térképet. Próbálja meg újra később." "Nem sikerült betölteni az üzeneteket" - "A(z) %1$s nem tudta elérni a tartózkodási helyét. Próbáld meg újra később." + "Az %1$s nem tudta elérni a tartózkodási helyét. Próbálja újra később." "Nem sikerült feltölteni a hangüzenetét." - "Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyedhez. Ezt a beállításokban engedélyezheted." + "Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Ezt a beállításokban engedélyezheti." "Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Engedélyezze alább az elérését." - "Az %1$snek nincs engedélye, hogy hozzáférjen a mikrofonhoz. Engedélyezd, hogy tudjon hangüzenetet felvenni." + "Az %1$snek nincs engedélye, hogy hozzáférjen a mikrofonhoz. Engedélyezze, hogy tudjon hangüzenetet felvenni." "Néhány üzenet nem került elküldésre" "Elnézést, hiba történt" "🔐️ Csatlakozz hozzám itt: %1$s" - "Beszélgessünk a(z) %1$s: %2$s -n" + "Beszélgessünk itt: %1$s, %2$s" "%1$s Android" "Az eszköz rázása a hibajelentéshez" - "A biztonságos üzenetkezelés beállításához ellenőrizze ezt az eszközt." - "Erősítse meg, hogy Ön az" - "Mostantól biztonságosan olvashat vagy küldhet üzeneteket, és bármelyik csevegőpartnere megbízhat ebben az eszközben." - "Eszköz ellenőrizve" "Nem sikerült kiválasztani a médiát, próbálja újra." "Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra." "Nem sikerült a média feltöltése, próbálja újra." @@ -257,6 +253,7 @@ "Letiltás" "A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat." "Felhasználó letiltása" + "Profil" "Letiltás feloldása" "Újra láthatja az összes üzenetét." "Felhasználó kitiltásának feloldása" diff --git a/libraries/ui-strings/src/main/res/values-in/translations.xml b/libraries/ui-strings/src/main/res/values-in/translations.xml index 5540e7b084..fdbf31da25 100644 --- a/libraries/ui-strings/src/main/res/values-in/translations.xml +++ b/libraries/ui-strings/src/main/res/values-in/translations.xml @@ -194,7 +194,7 @@ "Server tidak didukung" "URL Server" "Pengaturan" - "Membagikan lokasi" + "Lokasi terbagi" "Mengeluarkan dari akun" "Memulai obrolan…" "Stiker" @@ -241,10 +241,6 @@ "Hai, bicaralah dengan saya di %1$s: %2$s" "%1$s Android" "Rageshake untuk melaporkan kutu" - "Verifikasi perangkat ini untuk menyiapkan perpesanan aman." - "Konfirmasi bahwa ini Anda" - "Sekarang Anda dapat membaca atau mengirim pesan dengan aman, dan siapa pun yang mengobrol dengan Anda juga dapat mempercayai perangkat ini." - "Perangkat terverifikasi" "Gagal memilih media, silakan coba lagi." "Gagal memproses media untuk diunggah, silakan coba lagi." "Gagal mengunggah media, silakan coba lagi." @@ -253,6 +249,7 @@ "Blokir" "Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja." "Blokir pengguna" + "Profil" "Buka blokir" "Anda akan dapat melihat semua pesan dari mereka lagi." "Buka blokir pengguna" diff --git a/libraries/ui-strings/src/main/res/values-ru/translations.xml b/libraries/ui-strings/src/main/res/values-ru/translations.xml index 01a23b4c24..054d42b94f 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -179,7 +179,9 @@ "Политика конфиденциальности" "Реакция" "Реакции" - "Ключ восстановления" + + "Ключ восстановления" + "Обновление…" "Отвечает на %1$s" "Сообщить об ошибке" @@ -249,20 +251,15 @@ "Привет, поговори со мной по %1$s: %2$s" "%1$s Android" "Встряхните устройство, чтобы сообщить об ошибке" - "Подтвердите это устройство, чтобы настроить безопасный обмен сообщениями." - "Подтвердите, что это вы" - "Теперь вы можете безопасно читать и отправлять сообщения, и все, с кем вы общаетесь в чате, также могут доверять этому устройству." - "Устройство проверено" "Не удалось выбрать носитель, попробуйте еще раз." "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось загрузить медиафайлы, попробуйте еще раз." - "Сбой загрузки" - "Каталог комнат" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Не удалось получить данные о пользователе" "Заблокировать" "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время." "Заблокировать пользователя" + "Профиль" "Разблокировать" "Вы снова сможете увидеть все сообщения." "Разблокировать пользователя" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 9c1df72eaf..f777ed6b31 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -248,22 +248,15 @@ "Ahoj, porozprávajte sa so mnou na %1$s: %2$s" "%1$s Android" "Zúrivo potriasť pre nahlásenie chyby" - "Ak chcete nastaviť zabezpečené správy, overte toto zariadenie." - "Potvrďte, že ste to vy" - "Teraz môžete bezpečne čítať alebo odosielať správy a tomuto zariadeniu môže dôverovať aj ktokoľvek, s kým konverzujete." - "Zariadenie overené" - "Použiť iné zariadenie" - "Čaká sa na druhom zariadení…" "Nepodarilo sa vybrať médium, skúste to prosím znova." "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa nahrať médiá, skúste to prosím znova." - "Načítanie zlyhalo" - "Adresár miestností" "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Nepodarilo sa získať údaje o používateľovi" "Zablokovať" "Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať." "Zablokovať používateľa" + "Profil" "Odblokovať" "Všetky správy od nich budete môcť opäť vidieť." "Odblokovať používateľa" diff --git a/libraries/ui-strings/src/main/res/values-sv/translations.xml b/libraries/ui-strings/src/main/res/values-sv/translations.xml index 7ed1b0daa8..a7b1f295f3 100644 --- a/libraries/ui-strings/src/main/res/values-sv/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml @@ -96,6 +96,7 @@ "Redigerar" "* %1$s %2$s" "Kryptering aktiverad" + "Ange din PIN-kod" "Fel" "Alla" "Fil" @@ -143,6 +144,7 @@ "Riktextredigerare" "Rumsnamn" "t.ex. ditt projektnamn" + "Skärmlås" "Sök efter någon" "Sökresultat" "Säkerhet" @@ -164,6 +166,7 @@ "Kan inte avkryptera" "Inbjudan kunde inte skickas till en eller flera användare." "Kunde inte skicka inbjudningar" + "Lås upp" "Avtysta" "Händelse som inte stöds" "Användarnamn" @@ -182,6 +185,7 @@ "%1$s kunde inte komma åt din plats. Vänligen försök igen senare." "%1$s är inte behörig att komma åt din plats. Du kan aktivera åtkomst i Inställningar." "%1$s är inte behörig att komma åt din plats. Aktivera åtkomst nedan." + "%1$s är inte behörig att komma åt din mikrofon. Aktivera åtkomst för att spela in ett röstmeddelande." "Vissa meddelanden har inte skickats" "Tyvärr, ett fel uppstod" "🔐️ Häng med mig på %1$s" diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml index 542ff641aa..4082bfb6b0 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -51,7 +51,7 @@ "Відхилити" "Видалити опитування" "Вимкнути" - "Скасувати" + "Відкинути" "Готово" "Редагувати" "Редагувати опитування" @@ -60,7 +60,7 @@ "Введіть PIN-код" "Забули пароль?" "Переслати" - "Повернутися назад" + "Повернутися" "Запросити" "Запросити людей" "Запросити людей до %1$s" @@ -249,15 +249,9 @@ "Привіт, пишіть мені за адресою %1$s: %2$s" "%1$s Android" "Повідомити про ваду за допомогою Rageshake" - "Перевірте цей пристрій, щоб налаштувати безпечний обмін повідомленнями." - "Підтвердіть, що це ви" - "Тепер ви можете безпечно читати або надсилати повідомлення, і кожен, з ким ви спілкуєтесь, також може довіряти цьому пристрою." - "Пристрій перевірено" "Не вдалося вибрати медіафайл, спробуйте ще раз." "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." "Не вдалося завантажити медіафайл, спробуйте ще раз." - "Не вдалося завантажити" - "Каталог кімнат" "Не вдалося обробити медіафайл для завантаження, спробуйте ще раз." "Не вдалося отримати дані користувача" "Заблокувати" diff --git a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml index 872f21e82e..6f8df97530 100644 --- a/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh-rTW/translations.xml @@ -227,9 +227,6 @@ "有些訊息尚未傳送" "嘿,來 %1$s 和我聊天:%2$s" "%1$s Android" - "裝置已認證" - "使用另一個裝置" - "正在等待其他裝置……" "無法上傳媒體檔案,請稍後再試。" "封鎖" "封鎖使用者" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 97ddec4b3b..f19696065a 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -245,22 +245,15 @@ "Hey, talk to me on %1$s: %2$s" "%1$s Android" "Rageshake to report bug" - "Verify this device to set up secure messaging." - "Confirm that it\'s you" - "Now you can read or send messages securely, and anyone you chat with can also trust this device." - "Device verified" - "Use another device" - "Waiting on other device…" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." - "Failed loading" - "Room directory" "Failed processing media to upload, please try again." "Could not retrieve user details" "Block" "Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime." "Block user" + "Profile" "Unblock" "You\'ll be able to see all messages from them again." "Unblock user" diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index abc46f7d38..d1c61e4285 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -56,7 +56,7 @@ private const val versionMinor = 4 // 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 = 7 +private const val versionPatch = 8 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index cc254511bd..395f690d0e 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -114,6 +114,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:voicerecorder:impl")) implementation(project(":libraries:mediaplayer:impl")) implementation(project(":libraries:mediaviewer:impl")) + implementation(project(":libraries:troubleshoot:impl")) } fun DependencyHandlerScope.allServicesImpl() { diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/OnlyFallbackPermalinkParser.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/OnlyFallbackPermalinkParser.kt new file mode 100644 index 0000000000..50903a910d --- /dev/null +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/OnlyFallbackPermalinkParser.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.samples.minimal + +import android.net.Uri +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.libraries.matrix.api.permalink.PermalinkParser + +class OnlyFallbackPermalinkParser : PermalinkParser { + override fun parse(uriString: String): PermalinkData { + return PermalinkData.FallbackLink(Uri.parse(uriString)) + } + + override fun parse(uri: Uri): PermalinkData { + return PermalinkData.FallbackLink(uri) + } +} 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 e651e8f968..264a754e73 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 @@ -89,6 +89,7 @@ class RoomListScreen( ), profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider), + permalinkParser = OnlyFallbackPermalinkParser(), ), ) private val presenter = RoomListPresenter( @@ -118,7 +119,8 @@ class RoomListScreen( roomListService = matrixClient.roomListService, roomSummaryFactory = roomListRoomSummaryFactory, coroutineDispatchers = coroutineDispatchers, - ) + ), + featureFlagService = featureFlagService, ), sessionPreferencesStore = DefaultSessionPreferencesStore( context = context, @@ -150,12 +152,12 @@ class RoomListScreen( state = state, onRoomClicked = ::onRoomClicked, onSettingsClicked = {}, - onVerifyClicked = {}, onConfirmRecoveryKeyClicked = {}, onCreateRoomClicked = {}, onInvitesClicked = {}, onRoomSettingsClicked = {}, onMenuActionClicked = {}, + onRoomDirectorySearchClicked = {}, modifier = modifier, ) diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_10,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_10,NEXUS_5,1.0,de].png index 50c4629129..a5842d8949 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_10,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_10,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:728219bfadff0fd21612e1138d0fab253c48e5ed4519cfe3c1464f6f409822f5 -size 32534 +oid sha256:fe992d1369d3a248768364b60fffa62306bd66bb3e7a5ac41cc4ec62073da608 +size 37944 diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_2,NEXUS_5,1.0,de].png index 8b1638689e..7ef01ff89b 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_2,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_2,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ffea61ef0170d2d7486d1df32d7fe30782ddee9ce4d38dbc83b95f275583825 -size 43423 +oid sha256:64aec3a65c1ba5e0dc924dacc9fbeb54168cb1175f8d928e450fb3d46b546cba +size 48495 diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_3,NEXUS_5,1.0,de].png index 56bea9fdc6..d301549e63 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_3,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_3,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67418f15f47631ad73d2b5ed61e7fd782b56ee952d5cdd12e53950de58200349 -size 43973 +oid sha256:9576f73065257426d939cfff6a408cdd726852c93b924003315529fbe2d39697 +size 49068 diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_4,NEXUS_5,1.0,de].png index 460071abb3..bdae6a804f 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_4,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_4,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70129fc1a4cf8dd1407de2f42fd7c72d57e09853d66dc1c19e32903844b9046b -size 44456 +oid sha256:285fa01b64cc273806fe4e730a5f567d2f3304f7e7f6ecbb57282195f8bdf576 +size 49623 diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_5,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_5,NEXUS_5,1.0,de].png index 2f31bcf4d3..c42915ecd4 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_5,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_5,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbe8b24943045e3b3f042e16647dd6de60fe661ce14e012679ac7fbd2619ff49 -size 44094 +oid sha256:35657810c246fc3cce580e455ae95dd721064641e08ad358e80cbd48091055d4 +size 49203 diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_6,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_6,NEXUS_5,1.0,de].png index 8dab166e88..45b6da3076 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_6,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_6,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddd2210781391c12dcb67f1e2ec02ad5396a02212a9fa598ab3138a355450237 -size 44671 +oid sha256:6aec759b1855e1af48229b717a6c405206b8bcbc8306ed01f07cd5294f1fc0e5 +size 49869 diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_7,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_7,NEXUS_5,1.0,de].png index 17e78be76c..3f666c9660 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_7,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_7,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15baabcbe95da428dd360153397b90aca1acb263c079811b3890c8befd452e91 -size 45148 +oid sha256:0edc2123a648e859334a131c4f4a477e6e4843c0c83d052cf84b9e3843a534c6 +size 50393 diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_8,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_8,NEXUS_5,1.0,de].png index 5d271f0dbc..e0563748bf 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_8,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_8,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d17f08528c1f5e1b567fb3ec9302dfe571df6da7a0752e17d5686fb0b729778 -size 45389 +oid sha256:08703feba620c75bb0b09e565f907cedce0cabfe713bd285e30c3bb3b1ecbf11 +size 50673 diff --git a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_9,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_9,NEXUS_5,1.0,de].png index 37dbf17158..5fe1177ab1 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_9,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_9,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:079286f3b6a64c5162e7b7284f95df08ce51ae90eef58cb2403880e640d24024 -size 32638 +oid sha256:62da25de040bf0365765d3bf379f20b03eadff507abc7a323906459873e4e135 +size 37848 diff --git a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,de].png deleted file mode 100644 index 4cb65cc05e..0000000000 --- a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,de].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fda9297ea4cb7d4155250e69449b639fd0ff3207593b396eb4d749de7b6a6610 -size 17815 diff --git a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,de].png index 4cb65cc05e..647a20dcbf 100644 --- a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fda9297ea4cb7d4155250e69449b639fd0ff3207593b396eb4d749de7b6a6610 -size 17815 +oid sha256:75e762734dc8987ebe94bbe1b0be26febc3250ed001b5c140beca45b1b50b3ef +size 19548 diff --git a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_1,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_0,NEXUS_5,1.0,de].png deleted file mode 100644 index c940b4c0a9..0000000000 --- a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_0,NEXUS_5,1.0,de].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:68127db252601c41e441936db57b2508961fa2ce2d686426b7d51af73e508d08 -size 24865 diff --git a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_2,NEXUS_5,1.0,de].png deleted file mode 100644 index 2349388fce..0000000000 --- a/screenshots/de/ui_T_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_2,NEXUS_5,1.0,de].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:483a7fbb9d53f7fe0102f690dc75693a311dc733b25865285a8ef222e23a52bf -size 16219 diff --git a/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Day-4_5_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Day-4_5_null,NEXUS_5,1.0,de].png index a120bae4ac..f97428198e 100644 --- a/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Day-4_5_null,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Day-4_5_null,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee2f07fb883d4fe98e4eb9b31b8f744bfb8ac7e4475e3e899417ff8921282218 -size 23326 +oid sha256:5a1ee722b6d18ad044447dbf05ff4b45c27c1c13696a4bf2d40bd61216e5f09e +size 23606 diff --git a/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Day-5_6_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Day-5_6_null,NEXUS_5,1.0,de].png index e60fbfd469..1b3a9012f0 100644 --- a/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Day-5_6_null,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Day-5_6_null,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebafcfc81cdef83c70ac4f19bbcfe2a98c7aed9e9756ca6543cd352868a8cce2 -size 23335 +oid sha256:2b9c46336519829da81df6b3e1e31edc1fd8e5bda5d5c610f252525605f6826b +size 23611 diff --git a/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Day-12_13_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Day-12_13_null,NEXUS_5,1.0,de].png index d7c69a27b8..73dd0d0276 100644 --- a/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Day-12_13_null,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Day-12_13_null,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c145620dcad2c1c006447617b6dd255c8b480d43923b678074299a3774db3588 -size 50269 +oid sha256:b55eb3b44d7f133c381d98a1c29dfaae5913523e8d0edb618ffa429d52b656fd +size 50717 diff --git a/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Day-9_10_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Day-9_10_null,NEXUS_5,1.0,de].png index d7c69a27b8..73dd0d0276 100644 --- a/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Day-9_10_null,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Day-9_10_null,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c145620dcad2c1c006447617b6dd255c8b480d43923b678074299a3774db3588 -size 50269 +oid sha256:b55eb3b44d7f133c381d98a1c29dfaae5913523e8d0edb618ffa429d52b656fd +size 50717 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-8_9_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-7_8_null,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-8_9_null,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-7_8_null,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_0,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_0,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_0,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_1,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_1,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_1,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_2,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_2,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_2,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_3,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_3,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_3,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_4,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_4,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_4,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,de].png deleted file mode 100644 index dfde20564b..0000000000 --- a/screenshots/de/ui_T_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,de].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:78b94b76cae3c8fa8c1a6ffe3091d341c1091060704511533b1953a76a51b396 -size 45500 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,de].png index 5a0c83f920..f11863d7fd 100644 --- a/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bbda51850ec23c8c42491514119c3a0d538a32741811117c07c719f4e965857 -size 74496 +oid sha256:7017409c08b0b6514662e196af52899964d402ae633b96349e41a48b2b1209b0 +size 78040 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,de].png index 8efef115f6..7671323196 100644 --- a/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e800c4dad8273d778203549cb87938fa816a77dc95a6069308e004de98c451c8 -size 65231 +oid sha256:f4f2b3bacf7ab1dc5cc1bae40435657667c57d281d222361d0735c574dd69840 +size 68245 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,de].png index bec7bc96c4..2d98885ee5 100644 --- a/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60f3ff7b535f42e340c6433fef396a2404f1a7a2f4ca5b8963e5d42db38a43b0 -size 63875 +oid sha256:f014ca9304e364df7abd6b02fbbe675d5c441c91d010401afb2d32459a2479bc +size 66888 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_3,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..f594c73c43 --- /dev/null +++ b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_3,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0cd4e3d402a0c63c544810d7cff3feb69e0d744d4025fab5d4f143ee5a1692c +size 51253 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_4,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..88cc7fee19 --- /dev/null +++ b/screenshots/de/ui_T_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_4,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:195dd966b8f49562ab3d4889fe69b15836eb40950bd3942d8361db5655d3eb2d +size 46561 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,de].png index 7e990491d5..0e49c40ccc 100644 --- a/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2ca4270fd5b0f00d1216bfae06f2fccfa9f90496b0d85635553f440384ebb25 -size 40244 +oid sha256:23f2fc8ed789f22d63925e23ade89babed788e34fd6ff1c8dc08be3b66895833 +size 37976 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,de].png index c0b56ea96d..52bdb05f22 100644 --- a/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f8345064c114e7878e7013442fd05293ca1bd07ffbbdc1d0abe581853048eba -size 39866 +oid sha256:7773e3f5084a8d220874491b3a054dbf31fb009238f5449808e09b5116e4dd07 +size 37633 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,de].png index 37da265dde..f23ef4afd3 100644 --- a/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3339478f9d1fe1b6358e791bf44090584d1148da812e6f6d70a3f7b19b0715f -size 42241 +oid sha256:b54582b2e22a65ba2821dc235170643d958926f95d38352309c116d3ee2f7eb1 +size 39765 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,de].png index 2eab0820ae..b45c5e1ddc 100644 --- a/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fed8a895dd45102ad84a2376f94e04441d4506e82cb7ef76fc6163566482f3be -size 42184 +oid sha256:b365970794bf20e9b18885293fd9b200177790e197f988f2957ce66108ac3c53 +size 39713 diff --git a/screenshots/de/ui_T_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-11_12_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-10_11_null_0,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-11_12_null_0,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-10_11_null_0,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_3,NEXUS_5,1.0,de].png index 6fbf0729df..2d34e5cf5b 100644 --- a/screenshots/de/ui_T_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_3,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomdetails.impl.members.moderation_RoomMembersModerationView_null_RoomMembersModerationView-Day-4_5_null_3,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:871bbfeac8a04eb9ca756f660c3fa4cc235fd293ce10be4e0168cb0ab3b271e8 -size 10931 +oid sha256:574b5719f3a8a6c6c146e911912f8dc0e49d0321c6c9a7cd49b2a416c2bbc706 +size 10966 diff --git a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,de].png index c3f21c27af..e9304a0c6d 100644 --- a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94ef2559a5dc4ea875bfaa17a7d624a38f67531d2170bc0ac8d4ddf62a3e3408 -size 49340 +oid sha256:41a58b2117148c08802623a3349e2d90a75de1729f24188fe5ce3fbe4e8b28ff +size 49356 diff --git a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,de].png index 20f0ddea08..b187ebb903 100644 --- a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2ce6422668e29806424288ae87866514854113c7126356420fd43b096c4582a -size 50140 +oid sha256:d71540b3cf7c4bf556f3302df8491e4813a091ce2545e59b9d384f56f662a00e +size 50167 diff --git a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,de].png index 8e4d955cff..f91a8d5b0f 100644 --- a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a8b38d9d8c8900fd40db8f6de74a35cbdd7103f54b290a78d99b66ceda6cc21 -size 49238 +oid sha256:dbe7a2f33256517f91f6987c7337f4bc76fa411fbdec79b538ab16541383bf44 +size 49244 diff --git a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,de].png index 528c103c51..fff7896550 100644 --- a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ded9cc9e3ae1cdccd3131b332f5c30dd5b4a69317e0fcb989769f12629da147a -size 46428 +oid sha256:420ec56dfbe5948890ee7467b5803bf9ed10cb141143880e7ff1ee7f4f7d3db6 +size 46409 diff --git a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,de].png index f10530582c..282bd17123 100644 --- a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54bf9f8c0fcb9efed97939c95b3f533aad9e0adb5da3bd66e2543069dd021425 -size 46855 +oid sha256:1431c75b6105069c619b11f4f50206c0ff48732c5007234953654bbdd5ef147e +size 46847 diff --git a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,de].png index da24ac7b3b..fe1351eba4 100644 --- a/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cfc7b57fa6b9882a578380a6a634ddb8e360662df03088daff63dca5d91489e -size 56272 +oid sha256:9e86f66ffe933c56af20be8301c880950536f420fd99d6e89ffae6162352a86d +size 56256 diff --git a/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..3ee3be0d41 --- /dev/null +++ b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b6f0ea121612aa4023e9a0c6e14fe19615f07c93d94cfe1539d1d967dd81f72 +size 15061 diff --git a/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..c7c601331a --- /dev/null +++ b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931dec4c0491a88b28186878bda2093d1d51be8d19147308ba6a54dbf6445339 +size 31059 diff --git a/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..f49c79ff2d --- /dev/null +++ b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:584e37736a964a4a1b009859e0a660d0615e13241a470fee8406a53b93c9f01c +size 32912 diff --git a/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..99ca038987 --- /dev/null +++ b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f40d8cb6f0b73e30ecef7c354db23a3c9b4054e7e8cdd51e2a4287401a255f76 +size 31968 diff --git a/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..8f4560115b --- /dev/null +++ b/screenshots/de/ui_T_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ed001056babb81e004f455b45645c8b4a88c094d4cf2c189304ae42c5beffdf +size 36169 diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-8_9_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-8_9_null,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-7_8_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-7_8_null,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Day-5_6_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Day-5_6_null,NEXUS_5,1.0,de].png deleted file mode 100644 index 553fb2a5ec..0000000000 --- a/screenshots/de/ui_T_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Day-5_6_null,NEXUS_5,1.0,de].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:86a3af68fabec8f77e76b8832f20db7919b8ed6d5401b826abd3a2ed1adc0893 -size 34620 diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_1,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_1,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_1,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_3,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_3,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_3,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_4,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_4,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_4,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_0,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_1,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-12_13_null,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-12_13_null,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..cf0e5444de --- /dev/null +++ b/screenshots/de/ui_T_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4f23c0a8a77a1d0e4fa4278c5cec60b53e4b337d9076d450bdbbff7f6dba470 +size 11778 diff --git a/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,de].png similarity index 100% rename from screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,de].png rename to screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,de].png diff --git a/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,de].png index 72013a011f..db9841dce2 100644 --- a/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71ea12d0197945317a28e01e8b5b4bb198df0a6905014cc92fa0ca89b06cd991 -size 94061 +oid sha256:ce710e354bfdf64f656d728995ed163bca87ba69f325f583d76d6160682647e9 +size 96407 diff --git a/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,de].png index db9841dce2..6c30d2bd81 100644 --- a/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce710e354bfdf64f656d728995ed163bca87ba69f325f583d76d6160682647e9 -size 96407 +oid sha256:9ecc13b764d283ac5818273f96dc11d655c8e27fd914c79a9c36ebf99bcdf338 +size 59999 diff --git a/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,de].png deleted file mode 100644 index 6c30d2bd81..0000000000 --- a/screenshots/de/ui_T_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,de].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ecc13b764d283ac5818273f96dc11d655c8e27fd914c79a9c36ebf99bcdf338 -size 59999 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,de].png index 0699bd5473..a710f79532 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef5aedf5673d9a2345089e6632460cb12434cc6901e0d1cb86155e5b6dfe5968 -size 47359 +oid sha256:38a94d2276af933d4c123a2a00728db01be7d6665311217ca7f0124612b60105 +size 58662 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,de].png index f1eabc0045..d89ff64056 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6cd0d7eb7c90cf8a72818c1179e0c296db3eaeec82bf869f86170813cb4b002 -size 58807 +oid sha256:8e7e7263a120db2e187828203198188a87dd507701dc42b281d1c3d017454f00 +size 70414 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,de].png index df01ad7a03..186132354d 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fba1838d134534f42f30e22a1c4ac1eb3f8cebbf73aa45dd142cc52c313f369 -size 57250 +oid sha256:9fe482de11f11ff7f0bc4050b7ff143497e85ae7c6d12ba73ddfae3c32249e38 +size 68729 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,de].png index 0e7f4d6b6c..e250398dc2 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6d85294b3b206b14878d038efe0a9d8cee63659113fbe47816531962a180b08 -size 47712 +oid sha256:15c41ad7fa311752c3fc7b6d6e27f4f80944c6430fb6f23c659b9d795ec0cca0 +size 56295 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_0,NEXUS_5,1.0,de].png index c290043657..8822a44d49 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dde7780e24eb315b8671ada81dfc93397a2c7f324198d66cd17046d12d1ac3e0 -size 29115 +oid sha256:4ee264d6715fe4d8e8c0ff5ba07afcd33409d131d27effa77e07bbdeef47e23b +size 31034 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_1,NEXUS_5,1.0,de].png index 041e38b060..4741997df4 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ca3b63c4d12ef8d6436586043a3fd3cf80203c0c5d37f30c7027a3b11046188 -size 26185 +oid sha256:f4001ba3ca38363142f096aa4b7a052a8b7641982994698b5dd3134d0a79b65f +size 27951 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_10,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_10,NEXUS_5,1.0,de].png index 98522d2806..a40adf773a 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_10,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_10,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f21aac270d15f5c09cddfdc7dbd797b64acf612b979f1e4a629bb0f960d512a -size 26997 +oid sha256:1b1de5b9084a41c3e97d21aeb24ae14f761410084c950946085f69510da0bbc4 +size 39496 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_11,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_11,NEXUS_5,1.0,de].png index dec734c242..6c35e48ccb 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_11,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_11,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e232511aef5dd5ba6485876fbda79396ab292ca73fa0e88f73818bb562a5f4d8 -size 25036 +oid sha256:dce9eb56dc9c07027064147cd73adea4625e997575d7c4afaa453ede7c7a383c +size 36921 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_2,NEXUS_5,1.0,de].png index d7e0800c88..793849862c 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_2,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_2,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bba005d1b6341246ac3ddc03e1dcd7abecf9f80fca5bbf12e1b88d8a8ff8352d -size 28283 +oid sha256:8a736d35d4a326dfa007026ab1fdabf4d0b65c28f0bf2267c707362b55add6d6 +size 29891 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_3,NEXUS_5,1.0,de].png index d7e0800c88..793849862c 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_3,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_3,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bba005d1b6341246ac3ddc03e1dcd7abecf9f80fca5bbf12e1b88d8a8ff8352d -size 28283 +oid sha256:8a736d35d4a326dfa007026ab1fdabf4d0b65c28f0bf2267c707362b55add6d6 +size 29891 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_4,NEXUS_5,1.0,de].png index 9edbce8972..1f9fc0a2a7 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_4,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_4,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ffef63d7b63588193e346713f4898bd5b7cf5e8103a9440662f56e7b4897d38 -size 30826 +oid sha256:3cad5a954b9cfe40b1be19770e9f721d8d8d7f1f86d8c837c770ff4676bdab0c +size 33060 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_5,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_5,NEXUS_5,1.0,de].png index 041e38b060..4741997df4 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_5,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_5,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ca3b63c4d12ef8d6436586043a3fd3cf80203c0c5d37f30c7027a3b11046188 -size 26185 +oid sha256:f4001ba3ca38363142f096aa4b7a052a8b7641982994698b5dd3134d0a79b65f +size 27951 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_6,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_6,NEXUS_5,1.0,de].png index d7e0800c88..793849862c 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_6,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_6,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bba005d1b6341246ac3ddc03e1dcd7abecf9f80fca5bbf12e1b88d8a8ff8352d -size 28283 +oid sha256:8a736d35d4a326dfa007026ab1fdabf4d0b65c28f0bf2267c707362b55add6d6 +size 29891 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_7,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_7,NEXUS_5,1.0,de].png index d7e0800c88..793849862c 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_7,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_7,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bba005d1b6341246ac3ddc03e1dcd7abecf9f80fca5bbf12e1b88d8a8ff8352d -size 28283 +oid sha256:8a736d35d4a326dfa007026ab1fdabf4d0b65c28f0bf2267c707362b55add6d6 +size 29891 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_8,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_8,NEXUS_5,1.0,de].png index 56921f903c..51cf877cf8 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_8,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_8,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d43b1d6fb6b0c5bd0e7543f2eb27dec8361e2a8125c1935312cc58dedec3903c -size 16357 +oid sha256:994c6c3f0698d90e42bc5db279ad717a30b37a4bdcb068cfa22998f524dcf7aa +size 28167 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_9,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_9,NEXUS_5,1.0,de].png index a3b697b5ca..c6a3f32d68 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_9,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_9,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69fef43cf9f89ef995ae5679bd52178ee294ee604a2607eb6b9dd42e3817879b -size 16059 +oid sha256:bd781c1b0b295f8bc18db6ef031f37ad69cb3af4f905bc82859d9678fc43a58c +size 28005 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_0,NEXUS_5,1.0,de].png index 9348b1ee75..0a0bcdc725 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d6170a0b30d26402d4a3ad0b03726bd1b6516b8be11df9afdc1f0694b335e3d -size 71555 +oid sha256:fceb004153e45a443c2c3757e40e9743b0036f6a5fbd859d5b2ae9c1c3ba8903 +size 73614 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_1,NEXUS_5,1.0,de].png index 049e722748..de891bf928 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:097cb77dfb0a94e55df368d1d9048ec644e6dd3e7c82e8d214f80deb1e2fbffa -size 69111 +oid sha256:a831a3d420e4915afb0491d970080546e64c377bf0282d323583f600a6555503 +size 71210 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_2,NEXUS_5,1.0,de].png index 798dc4d146..756033069b 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_2,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_2,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3e472b79bebfdc8b8c3ba87a1f1f8fa7570fd2f782dbb3b890f50f5845140aa -size 70567 +oid sha256:5bb0151e365ea9b716b6e5ccf6c8306d666bc356b23f290694fd1d6113482d25 +size 72598 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_3,NEXUS_5,1.0,de].png index 798dc4d146..756033069b 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_3,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_3,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3e472b79bebfdc8b8c3ba87a1f1f8fa7570fd2f782dbb3b890f50f5845140aa -size 70567 +oid sha256:5bb0151e365ea9b716b6e5ccf6c8306d666bc356b23f290694fd1d6113482d25 +size 72598 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_0,NEXUS_5,1.0,de].png index 80f7d4f7b0..2c33e75e06 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bee775c1a68bd219ce0b64556d6ce2b03f62daff98c11e4a28666387b89dbbb -size 74456 +oid sha256:495fb8d17b8cae38ea7c3861014a96cff4b30200d537061b867aa9fc868c8adf +size 76560 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_1,NEXUS_5,1.0,de].png index 0a5aabccec..7a85cfc26a 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afcc930123227d55b6151258f6fbad80e06ab0e81cad5361c58b1ab62a75e7ad -size 71030 +oid sha256:75003b0a3d79c90823f2f5509faa8461c51d00e740d787c26621d3125b14e88f +size 73120 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_2,NEXUS_5,1.0,de].png index 798dc4d146..756033069b 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_2,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_2,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3e472b79bebfdc8b8c3ba87a1f1f8fa7570fd2f782dbb3b890f50f5845140aa -size 70567 +oid sha256:5bb0151e365ea9b716b6e5ccf6c8306d666bc356b23f290694fd1d6113482d25 +size 72598 diff --git a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_3,NEXUS_5,1.0,de].png index 798dc4d146..756033069b 100644 --- a/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_3,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_3,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3e472b79bebfdc8b8c3ba87a1f1f8fa7570fd2f782dbb3b890f50f5845140aa -size 70567 +oid sha256:5bb0151e365ea9b716b6e5ccf6c8306d666bc356b23f290694fd1d6113482d25 +size 72598 diff --git a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,de].png index efdb436f69..689b45e144 100644 --- a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3bb899e721e1480c992b6291668735f94feca41683412684584cd6e25e70cf0 -size 32778 +oid sha256:75cc1c4a706be9cd7faf01eb32a4a052fac47cf1c4895a1dce1e1e6e829dc156 +size 36294 diff --git a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,de].png index 348738d731..ff32bc7d30 100644 --- a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d12e9eac6b07bbed59da78f5efd7047c719ff1290e712f25b5f6e3a0a6c034f3 -size 33270 +oid sha256:0456bc9ec15195f7c16d46289e8fc13f19a739ba422e1ca10888a4ecc06a1632 +size 30540 diff --git a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,de].png index 232063f4dd..d85f60f604 100644 --- a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2f9c4f6f3cd5c24f2bf1492552b0a22aee3cc0ddb0e7a85b5c4b2ff4756a5c1 -size 58247 +oid sha256:7023f599e76cb8cf68825de152d5a60c8fbeedd6993c2cfbe57eadcbbb970642 +size 57857 diff --git a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,de].png index ca35ca6ed9..230db21507 100644 --- a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:880745e2ef6e19abb927e6ca7e20b68f6d851d6ac4cab1eb6c9c6b4cff08dea1 -size 59568 +oid sha256:821a476d4c2c78316d9bf16ef5843609a9b6ae31880cb1440fb94c081a651055 +size 59640 diff --git a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,de].png index 65b40482ff..abc3b326d9 100644 --- a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7af6f637922df62df93b983d3d890cdd7bb0c504c1c02ca97057e148bb3d5c0 -size 39498 +oid sha256:8fa22e63f524bde8bf0f9c725f5fd2fe7f9535ffdd7ee9870988af34977116ff +size 38591 diff --git a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,de].png index 4085ba2d44..591213011a 100644 --- a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f763f7577ada3240f81004180bd9fb45b3cb06ddb002bb086301e474e7c279c2 -size 22660 +oid sha256:88ae7a0c26283298fed235e8f1e743df177660925e9a7c5ce668f52be6a4c55e +size 24420 diff --git a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,de].png index 37d64cbcd6..0c6ed24723 100644 --- a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b31e2bcae16f93242a749bb1ce084376029ca2d048505fef25d7d146eb43b25c -size 39761 +oid sha256:35b2b295cf16494a9f84cb27d172db25295e4c924f55d5d33642db4914ff12a5 +size 39248 diff --git a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,de].png index a358b43d8d..689b45e144 100644 --- a/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,de].png +++ b/screenshots/de/ui_T_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,de].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d23113da0ef829afc1157c5482a44839aa8a2bcc023a54cae1ad3ddbe813592d -size 39994 +oid sha256:75cc1c4a706be9cd7faf01eb32a4a052fac47cf1c4895a1dce1e1e6e829dc156 +size 36294 diff --git a/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_0,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_0,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..a4ab15b039 --- /dev/null +++ b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_0,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7372c66ad820f04a14c61fa942cb9a7870f56e46dec4599bda70a64df874879 +size 38440 diff --git a/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_1,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_1,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..9c6d0c83cb --- /dev/null +++ b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_1,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5785b28544fc4dafe0ad17a91b55adc6c7a2c75f0da8c0e6e1def25773fe30bc +size 21937 diff --git a/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_2,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_2,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..29e7b23b6b --- /dev/null +++ b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_2,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8acbb368bd44d83ca168a55e4e6b94905aeb19efbf41dd4266dcfcf95cff383 +size 22298 diff --git a/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_3,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_3,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..b6904bbfbc --- /dev/null +++ b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_3,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1efede30d3c36c9a1fa2f1e0cae6ff22a38dfb52050444e7a51d46aba5d838d2 +size 35318 diff --git a/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_4,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_4,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..b265245f27 --- /dev/null +++ b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_4,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4282811a5864f9d41502b7108888bda5b63fd3b0f258ad2b74f10d44ba77d7e +size 30210 diff --git a/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_5,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_5,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..6e491c3eb5 --- /dev/null +++ b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_5,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca34ce2d563084f808a5ba09e6ca2dd677b266be079a71ed3c204df7138c27ec +size 47196 diff --git a/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_6,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_6,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..b2dfd012bf --- /dev/null +++ b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_6,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0175a2cfae9b0b26872f637bc840ca65d54e9e55d4e3e1be89f97ebda29f4f8f +size 29466 diff --git a/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_7,NEXUS_5,1.0,de].png b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_7,NEXUS_5,1.0,de].png new file mode 100644 index 0000000000..53f805a796 --- /dev/null +++ b/screenshots/de/ui_T_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_7,NEXUS_5,1.0,de].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c16acd3ecb3b0548e28ffde3c9a4bfba87a1f7d50458a662c80ef51cbd5daf3f +size 28075 diff --git a/screenshots/html/data.js b/screenshots/html/data.js index 267730e8a0..f388d4bad6 100644 --- a/screenshots/html/data.js +++ b/screenshots/html/data.js @@ -100,6 +100,9 @@ export const screenshots = [ ["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_58,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_59,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_6,NEXUS_5,1.0,en]","",0,], +["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en]","",0,], +["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en]","",0,], +["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_7,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_8,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_9,NEXUS_5,1.0,en]","",0,], @@ -196,9 +199,9 @@ export const screenshots = [ ["ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_2,NEXUS_5,1.0,en]","ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_2,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_3,NEXUS_5,1.0,en]","ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_3,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Day-1_2_null_4,NEXUS_5,1.0,en]","ui_S_t[f.logout.impl.direct_DefaultDirectLogoutView_null_DefaultDirectLogoutView-Night-1_3_null_4,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-8_9_null,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-8_10_null,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-8_9_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-8_10_null,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-7_8_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-7_9_null,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-7_8_null,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-7_9_null,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-7_9_null,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-6_8_null,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_0,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_1,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Day-4_5_null_2,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.developer_DeveloperSettingsView_null_DeveloperSettingsView-Night-4_6_null_2,NEXUS_5,1.0,en]",1,], @@ -213,21 +216,19 @@ export const screenshots = [ ["ui_S_t[l.designsystem.text_DpScale_1_0f__null_DpScale_1_0f__0_null,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.text_DpScale_1_5f__null_DpScale_1_5f__0_null,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.theme.components_DropdownMenuItem_null_Menus_DropdownMenuItem_0_null,NEXUS_5,1.0,en]","",0,], -["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_0,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_0,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_1,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_1,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_2,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_2,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_3,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_3,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_4,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_4,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-11_12_null_0,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-11_13_null_0,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_0,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_0,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_1,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_1,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_2,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_2,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_3,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_3,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_4,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_4,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-10_11_null_0,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-10_12_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[l.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_null_ElementLogoAtomLargeNoBlurShadow-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.atoms_ElementLogoAtomLargeNoBlurShadow_null_ElementLogoAtomLargeNoBlurShadow-Night_1_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.atomic.atoms_ElementLogoAtomLarge_null_ElementLogoAtomLarge-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.atoms_ElementLogoAtomLarge_null_ElementLogoAtomLarge-Night_1_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_null_ElementLogoAtomMediumNoBlurShadow-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.atoms_ElementLogoAtomMediumNoBlurShadow_null_ElementLogoAtomMediumNoBlurShadow-Night_1_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.atomic.atoms_ElementLogoAtomMedium_null_ElementLogoAtomMedium-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.atoms_ElementLogoAtomMedium_null_ElementLogoAtomMedium-Night_1_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Day-31_31_null,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Night-31_32_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Day-32_32_null,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Night-32_33_null,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_0,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_0,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_1,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_1,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_2,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_2,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null,NEXUS_5,1.0,en]",1,], ["ui_S_t[l.designsystem.components.dialogs_ErrorDialogContent_null_Dialogs_ErrorDialogContent_0_null,NEXUS_5,1.0,en]","",1,], ["ui_S_t[l.designsystem.components.dialogs_ErrorDialog_null_ErrorDialog-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.components.dialogs_ErrorDialog_null_ErrorDialog-Night_1_null,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-62_62_null,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-62_63_null,NEXUS_5,1.0,en]",0,], @@ -247,6 +248,7 @@ export const screenshots = [ ["ui_S_t[l.designsystem.theme.components_IconButton_null_Buttons_IconButton_0_null,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.theme.components_IconImageVector_null_Icons_IconImageVector_0_null,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_null_IconTitlePlaceholdersRowMolecule-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_null_IconTitlePlaceholdersRowMolecule-Night_1_null,NEXUS_5,1.0,en]",0,], +["ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMoleculeWithResIcon_null_IconTitleSubtitleMoleculeWithResIcon-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMoleculeWithResIcon_null_IconTitleSubtitleMoleculeWithResIcon-Night_1_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMolecule_null_IconTitleSubtitleMolecule-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMolecule_null_IconTitleSubtitleMolecule-Night_1_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.theme.components_IconToggleButton_null_Toggles_IconToggleButton_0_null,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_0,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_0,NEXUS_5,1.0,en]",0,], @@ -257,7 +259,6 @@ export const screenshots = [ ["ui_S_t[l.designsystem.icons_IconsOther_null_IconsOther-Day_0_null_0,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.icons_IconsOther_null_IconsOther-Night_1_null_0,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Night_1_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Night_1_null,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Night-7_9_null,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_0,NEXUS_5,1.0,en]","ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_1,NEXUS_5,1.0,en]","ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Day-0_1_null_2,NEXUS_5,1.0,en]","ui_S_t[f.invitelist.impl_InviteListView_null_InviteListView-Night-0_2_null_2,NEXUS_5,1.0,en]",1,], @@ -411,7 +412,7 @@ export const screenshots = [ ["ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-12_13_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-12_14_null,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-11_13_null,NEXUS_5,1.0,en]",1,], ["ui_S_t[l.designsystem.theme.components_ModalBottomSheetDark_null_BottomSheets_ModalBottomSheetDark_0_null,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.theme.components_ModalBottomSheetLayoutDark_null_BottomSheets_ModalBottomSheetLayoutDark_0_null,NEXUS_5,1.0,en]","",0,], ["ui_S_t[l.designsystem.theme.components_ModalBottomSheetLayoutLight_null_BottomSheets_ModalBottomSheetLayoutLight_0_null,NEXUS_5,1.0,en]","",0,], @@ -424,6 +425,8 @@ export const screenshots = [ ["ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_2,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_3,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_3,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_4,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_4,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en]","ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.login.impl.oidc.webview_OidcView_null_OidcView-Day-3_4_null_0,NEXUS_5,1.0,en]","ui_S_t[f.login.impl.oidc.webview_OidcView_null_OidcView-Night-3_5_null_0,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.login.impl.oidc.webview_OidcView_null_OidcView-Day-3_4_null_1,NEXUS_5,1.0,en]","ui_S_t[f.login.impl.oidc.webview_OidcView_null_OidcView-Night-3_5_null_1,NEXUS_5,1.0,en]",0,], @@ -529,10 +532,9 @@ export const screenshots = [ ["ui_S_t[f.messages.impl.report_ReportMessageView_null_ReportMessageView-Day-7_7_null_3,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.report_ReportMessageView_null_ReportMessageView-Night-7_8_null_3,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.messages.impl.report_ReportMessageView_null_ReportMessageView-Day-7_7_null_4,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.report_ReportMessageView_null_ReportMessageView-Night-7_8_null_4,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.messages.impl.report_ReportMessageView_null_ReportMessageView-Day-7_7_null_5,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.report_ReportMessageView_null_ReportMessageView-Night-7_8_null_5,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Day-5_6_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Night-5_7_null,NEXUS_5,1.0,en]",1,], ["ui_S_t[l.designsystem.components.dialogs_RetryDialogContent_null_Dialogs_RetryDialogContent_0_null,NEXUS_5,1.0,en]","",1,], ["ui_S_t[l.designsystem.components.dialogs_RetryDialog_null_RetryDialog-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.components.dialogs_RetryDialog_null_RetryDialog-Night_1_null,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_0,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_0,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Day-8_9_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.rolesandpermissions_RolesAndPermissionView_null_RolesAndPermissionView-Night-8_10_null_1,NEXUS_5,1.0,en]",1,], @@ -569,6 +571,11 @@ export const screenshots = [ ["ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_7,NEXUS_5,1.0,en]","",1,], ["ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_8,NEXUS_5,1.0,en]","",1,], ["ui_S_t[f.roomdetails.impl_RoomDetails_null_RoomDetails--0_0_null_9,NEXUS_5,1.0,en]","",1,], +["ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en]","ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en]","ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Day-1_2_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Night-1_3_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Day-1_2_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Night-1_3_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Day-1_2_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Night-1_3_null_2,NEXUS_5,1.0,en]",1,], @@ -577,23 +584,23 @@ export const screenshots = [ ["ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Day-1_2_null_5,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Night-1_3_null_5,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Day-1_2_null_6,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Night-1_3_null_6,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Day-1_2_null_7,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.invite_RoomInviteMembers_null_RoomInviteMembers-Night-1_3_null_7,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_0,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_1,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_2,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_3,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_3,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_4,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_4,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-11_13_null_0,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-11_13_null_1,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_0,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_1,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_2,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_3,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_3,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_4,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_4,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_0,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Day-2_3_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContentForDm_null_RoomListModalBottomSheetContentForDm-Night-2_4_null,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Day-1_2_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListModalBottomSheetContent_null_RoomListModalBottomSheetContent-Night-1_3_null,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_0,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_1,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_0,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_2,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_0,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_1,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_2,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_3,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_3,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_4,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_4,NEXUS_5,1.0,en]",1,], @@ -601,7 +608,7 @@ export const screenshots = [ ["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_6,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_6,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en]",1,], +["ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Day-3_4_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Night-3_5_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Day-3_4_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Night-3_5_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Day-3_4_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomdetails.impl.members_RoomMemberBannedList_null_RoomMemberBannedList-Night-3_5_null_2,NEXUS_5,1.0,en]",1,], @@ -651,35 +658,35 @@ export const screenshots = [ ["ui_S_t[l.roomselect.impl_RoomSelectView_null_RoomSelectView-Day-0_1_null_2,NEXUS_5,1.0,en]","ui_S_t[l.roomselect.impl_RoomSelectView_null_RoomSelectView-Night-0_2_null_2,NEXUS_5,1.0,en]",1,], ["ui_S_t[l.roomselect.impl_RoomSelectView_null_RoomSelectView-Day-0_1_null_3,NEXUS_5,1.0,en]","ui_S_t[l.roomselect.impl_RoomSelectView_null_RoomSelectView-Night-0_2_null_3,NEXUS_5,1.0,en]",1,], ["ui_S_t[l.roomselect.impl_RoomSelectView_null_RoomSelectView-Day-0_1_null_4,NEXUS_5,1.0,en]","ui_S_t[l.roomselect.impl_RoomSelectView_null_RoomSelectView-Night-0_2_null_4,NEXUS_5,1.0,en]",1,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Day-9_10_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Night-9_11_null,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_0,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_1,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_10,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_10,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_11,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_11,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_12,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_12,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_13,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_13,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_14,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_14,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_15,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_15,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_16,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_16,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_17,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_17,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_18,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_18,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_19,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_19,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_2,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_20,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_20,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_21,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_21,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_22,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_22,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_23,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_23,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_24,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_24,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_25,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_25,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_26,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_26,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_27,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_27,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_3,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_3,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_4,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_4,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_5,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_5,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_6,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_6,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_7,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_7,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_8,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_8,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_9,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_9,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Day-8_9_null,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Night-8_10_null,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_0,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_0,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_1,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_1,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_10,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_10,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_11,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_11,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_12,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_12,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_13,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_13,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_14,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_14,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_15,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_15,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_16,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_16,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_17,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_17,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_18,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_18,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_19,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_19,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_2,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_2,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_20,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_20,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_21,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_21,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_22,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_22,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_23,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_23,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_24,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_24,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_25,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_25,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_26,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_26,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_27,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_27,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_3,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_3,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_4,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_4,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_5,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_5,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_6,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_6,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_7,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_7,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_8,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_8,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_9,NEXUS_5,1.0,en]","ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_9,NEXUS_5,1.0,en]",0,], ["ui_S_t[appnav.root_Root_null_Root-Day-3_3_null_0,NEXUS_5,1.0,en]","ui_S_t[appnav.root_Root_null_Root-Night-3_4_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[appnav.root_Root_null_Root-Day-3_3_null_1,NEXUS_5,1.0,en]","ui_S_t[appnav.root_Root_null_Root-Night-3_4_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[appnav.root_Root_null_Root-Day-3_3_null_2,NEXUS_5,1.0,en]","ui_S_t[appnav.root_Root_null_Root-Night-3_4_null_2,NEXUS_5,1.0,en]",1,], @@ -932,6 +939,14 @@ export const screenshots = [ ["ui_S_t[l.designsystem.components_TitleWithIconFull_null_TitleWithIconFull-Day_0_null_4,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.components_TitleWithIconFull_null_TitleWithIconFull-Night_1_null_4,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.components_TitleWithIconMinimal_null_TitleWithIconMinimal-Day_0_null,NEXUS_5,1.0,en]","ui_S_t[l.designsystem.components_TitleWithIconMinimal_null_TitleWithIconMinimal-Night_1_null,NEXUS_5,1.0,en]",0,], ["ui_S_t[l.designsystem.theme.components_TopAppBar_null_AppBars_TopAppBar_0_null,NEXUS_5,1.0,en]","",0,], +["ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_0,NEXUS_5,1.0,en]","ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_0,NEXUS_5,1.0,en]",1,], +["ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_1,NEXUS_5,1.0,en]","ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_1,NEXUS_5,1.0,en]",1,], +["ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_2,NEXUS_5,1.0,en]","ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_2,NEXUS_5,1.0,en]",1,], +["ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_3,NEXUS_5,1.0,en]","ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_3,NEXUS_5,1.0,en]",1,], +["ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_4,NEXUS_5,1.0,en]","ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_4,NEXUS_5,1.0,en]",1,], +["ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_5,NEXUS_5,1.0,en]","ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_5,NEXUS_5,1.0,en]",1,], +["ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_6,NEXUS_5,1.0,en]","ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_6,NEXUS_5,1.0,en]",1,], +["ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_7,NEXUS_5,1.0,en]","ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_7,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-64_64_null_0,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-64_65_null_0,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-64_64_null_1,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-64_65_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-64_64_null_2,NEXUS_5,1.0,en]","ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-64_65_null_2,NEXUS_5,1.0,en]",1,], @@ -955,9 +970,9 @@ export const screenshots = [ ["ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_6,NEXUS_5,1.0,en]","ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_6,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_7,NEXUS_5,1.0,en]","ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_7,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_8,NEXUS_5,1.0,en]","ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_8,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_0,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_0,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_1,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_1,NEXUS_5,1.0,en]",0,], -["ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_2,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_2,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_0,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_0,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_1,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_1,NEXUS_5,1.0,en]",0,], +["ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_2,NEXUS_5,1.0,en]","ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_2,NEXUS_5,1.0,en]",0,], ["ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en]","ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en]","ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en]",1,], ["ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en]","ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en]",1,], diff --git a/services/analytics/api/build.gradle.kts b/services/analytics/api/build.gradle.kts index 28b871a659..c78a050571 100644 --- a/services/analytics/api/build.gradle.kts +++ b/services/analytics/api/build.gradle.kts @@ -14,7 +14,7 @@ * limitations under the License. */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { @@ -23,6 +23,7 @@ android { dependencies { api(projects.services.analyticsproviders.api) + api(projects.services.toolbox.api) implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) implementation(projects.libraries.core) diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/ScreenTracker.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/ScreenTracker.kt new file mode 100644 index 0000000000..c29a045f77 --- /dev/null +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/ScreenTracker.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.analytics.api + +import androidx.compose.runtime.Composable +import im.vector.app.features.analytics.plan.MobileScreen + +interface ScreenTracker { + @Composable + fun TrackScreen( + screen: MobileScreen.ScreenName, + ) +} diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/JoinedRoomExt.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/JoinedRoomExt.kt index d46d474dfc..c8ab1fad06 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/JoinedRoomExt.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/JoinedRoomExt.kt @@ -17,8 +17,6 @@ package io.element.android.services.analytics.api.extensions import im.vector.app.features.analytics.plan.JoinedRoom -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.room.MatrixRoom fun Long?.toAnalyticsRoomSize(): JoinedRoom.RoomSize { @@ -34,9 +32,9 @@ fun Long?.toAnalyticsRoomSize(): JoinedRoom.RoomSize { fun MatrixRoom.toAnalyticsJoinedRoom(trigger: JoinedRoom.Trigger?): JoinedRoom { return JoinedRoom( - isDM = this.isDirect.orFalse(), - isSpace = MatrixPatterns.isSpaceId(this.roomId.value), - roomSize = this.joinedMemberCount.toAnalyticsRoomSize(), + isDM = isDirect, + isSpace = isSpace, + roomSize = joinedMemberCount.toAnalyticsRoomSize(), trigger = trigger ) } diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/ViewRoomExt.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/ViewRoomExt.kt index 3cbfdf7292..2845129fa8 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/ViewRoomExt.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/ViewRoomExt.kt @@ -17,16 +17,14 @@ package io.element.android.services.analytics.api.extensions import im.vector.app.features.analytics.plan.ViewRoom -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.room.MatrixRoom fun MatrixRoom.toAnalyticsViewRoom(trigger: ViewRoom.Trigger? = null, selectedSpace: MatrixRoom? = null, viaKeyboard: Boolean? = null): ViewRoom { val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home return ViewRoom( - isDM = this.isDirect.orFalse(), - isSpace = MatrixPatterns.isSpaceId(this.roomId.value), + isDM = isDirect, + isSpace = isSpace, trigger = trigger, activeSpace = activeSpace, viaKeyboard = viaKeyboard diff --git a/services/analytics/impl/build.gradle.kts b/services/analytics/impl/build.gradle.kts index 5dd72d77bd..623ab493c3 100644 --- a/services/analytics/impl/build.gradle.kts +++ b/services/analytics/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.architecture) + implementation(projects.libraries.designsystem) implementation(projects.libraries.sessionStorage.api) api(projects.services.analyticsproviders.api) diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultScreenTracker.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultScreenTracker.kt new file mode 100644 index 0000000000..c53757384d --- /dev/null +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultScreenTracker.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.services.analytics.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.lifecycle.Lifecycle +import com.squareup.anvil.annotations.ContributesBinding +import im.vector.app.features.analytics.plan.MobileScreen +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import io.element.android.libraries.di.AppScope +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.api.ScreenTracker +import io.element.android.services.toolbox.api.systemclock.SystemClock +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultScreenTracker @Inject constructor( + private val analyticsService: AnalyticsService, + private val systemClock: SystemClock +) : ScreenTracker { + @Composable + override fun TrackScreen( + screen: MobileScreen.ScreenName, + ) { + var startTime by remember { mutableLongStateOf(0L) } + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + startTime = systemClock.epochMillis() + } + Lifecycle.Event.ON_PAUSE -> analyticsService.screen( + screen = MobileScreen( + durationMs = (systemClock.epochMillis() - startTime).toInt(), + screenName = screen + ) + ) + else -> Unit + } + } + } +} diff --git a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/strings/FakeStringProvider.kt b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/strings/FakeStringProvider.kt index c9f9a84fc9..870b3acf39 100644 --- a/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/strings/FakeStringProvider.kt +++ b/services/toolbox/test/src/main/kotlin/io/element/android/services/toolbox/test/strings/FakeStringProvider.kt @@ -26,10 +26,10 @@ class FakeStringProvider( } override fun getString(resId: Int, vararg formatArgs: Any?): String { - return defaultResult + return defaultResult + formatArgs.joinToString() } override fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any?): String { - return defaultResult + return defaultResult + " ($quantity) " + formatArgs.joinToString() } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt new file mode 100644 index 0000000000..2735827134 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/PresenterTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext +import app.cash.turbine.test +import io.element.android.libraries.architecture.Presenter +import kotlin.time.Duration + +suspend fun Presenter.test( + timeout: Duration? = null, + name: String? = null, + validate: suspend TurbineTestContext.() -> Unit, +) { + moleculeFlow(RecompositionMode.Immediate) { + present() + }.test(timeout, name, validate) +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Assertions.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Assertions.kt new file mode 100644 index 0000000000..af241fdea0 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Assertions.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils.lambda + +fun assert(lambdaRecorder: LambdaRecorder): LambdaRecorderAssertions { + return lambdaRecorder.assertions() +} + +class LambdaRecorderAssertions internal constructor( + private val parametersSequence: List>, +) { + fun isCalledOnce(): CalledOnceParametersAssertions { + return CalledOnceParametersAssertions( + assertions = isCalledExactly(1) + ) + } + + fun isNeverCalled() { + isCalledExactly(0) + } + + fun isCalledExactly(times: Int): ParametersAssertions { + if (parametersSequence.size != times) { + throw AssertionError("Expected to be called $times, but was called ${parametersSequence.size} times") + } + return ParametersAssertions(parametersSequence) + } +} + +class CalledOnceParametersAssertions internal constructor(private val assertions: ParametersAssertions) { + fun with(vararg matchers: ParameterMatcher) { + assertions.withSequence(matchers.toList()) + } + + fun withNoParameter() { + assertions.withNoParameter() + } +} + +class ParametersAssertions internal constructor( + private val parametersSequence: List> +) { + fun withSequence(vararg matchersSequence: List) { + if (parametersSequence.size != matchersSequence.size) { + throw AssertionError("Lambda was called ${parametersSequence.size} times, but only ${matchersSequence.size} assertions were provided") + } + parametersSequence.zip(matchersSequence).forEachIndexed { invocationIndex, (parameters, matchers) -> + if (parameters.size != matchers.size) { + throw AssertionError("Expected ${matchers.size} parameters, but got ${parameters.size} parameters during invocation #$invocationIndex") + } + parameters.zip(matchers).forEachIndexed { paramIndex, (param, matcher) -> + if (!matcher.match(param)) { + throw AssertionError( + "Parameter #$paramIndex does not match the expected value (actual=$param,expected=$matcher) during invocation #$invocationIndex" + ) + } + } + } + } + + fun withNoParameter() { + if (parametersSequence.any { it.isNotEmpty() }) { + throw AssertionError("Expected no parameters, but got some") + } + } +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt new file mode 100644 index 0000000000..b7beaaa5e9 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils.lambda + +/** + * A recorder that can be used to record the parameters of lambda invocation. + */ +abstract class LambdaRecorder internal constructor( + private val assertNoInvocation: Boolean, +) { + private val parametersSequence: MutableList> = mutableListOf() + + internal fun onInvoke(vararg params: Any?) { + if (assertNoInvocation) { + throw AssertionError("This lambda should never be called.") + } + parametersSequence.add(params.toList()) + } + + fun assertions(): LambdaRecorderAssertions { + return LambdaRecorderAssertions(parametersSequence = parametersSequence) + } +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: () -> R +): LambdaNoParamRecorder { + return LambdaNoParamRecorder(ensureNeverCalled, block) +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T) -> R +): LambdaOneParamRecorder { + return LambdaOneParamRecorder(ensureNeverCalled, block) +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2) -> R +): LambdaTwoParamsRecorder { + return LambdaTwoParamsRecorder(ensureNeverCalled, block) +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2, T3) -> R +): LambdaThreeParamsRecorder { + return LambdaThreeParamsRecorder(ensureNeverCalled, block) +} + +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2, T3, T4) -> R +): LambdaFourParamsRecorder { + return LambdaFourParamsRecorder(ensureNeverCalled, block) +} + +class LambdaNoParamRecorder(ensureNeverCalled: Boolean, val block: () -> R) : LambdaRecorder(ensureNeverCalled), () -> R { + override fun invoke(): R { + onInvoke() + return block() + } +} + +class LambdaOneParamRecorder(ensureNeverCalled: Boolean, val block: (T) -> R) : LambdaRecorder(ensureNeverCalled), (T) -> R { + override fun invoke(p: T): R { + onInvoke(p) + return block(p) + } +} + +class LambdaTwoParamsRecorder(ensureNeverCalled: Boolean, val block: (T1, T2) -> R) : LambdaRecorder(ensureNeverCalled), (T1, T2) -> R { + override fun invoke(p1: T1, p2: T2): R { + onInvoke(p1, p2) + return block(p1, p2) + } +} + +class LambdaThreeParamsRecorder(ensureNeverCalled: Boolean, val block: (T1, T2, T3) -> R) : LambdaRecorder( + ensureNeverCalled +), (T1, T2, T3) -> R { + override fun invoke(p1: T1, p2: T2, p3: T3): R { + onInvoke(p1, p2, p3) + return block(p1, p2, p3) + } +} + +class LambdaFourParamsRecorder(ensureNeverCalled: Boolean, val block: (T1, T2, T3, T4) -> R) : LambdaRecorder( + ensureNeverCalled +), (T1, T2, T3, T4) -> R { + override fun invoke(p1: T1, p2: T2, p3: T3, p4: T4): R { + onInvoke(p1, p2, p3, p4) + return block(p1, p2, p3, p4) + } +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt new file mode 100644 index 0000000000..dd509ed262 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/ParameterMatcher.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils.lambda + +/** + * A matcher that can be used to match parameters in lambda calls. + * This is useful to assert that a lambda has been called with specific parameters. + */ +interface ParameterMatcher { + fun match(param: Any?): Boolean +} + +/** + * A matcher that matches a specific value. + * Can be used to assert that a lambda has been called with a specific value. + */ +fun value(expectedValue: T) = object : ParameterMatcher { + override fun match(param: Any?) = param == expectedValue + override fun toString(): String = "value($expectedValue)" +} + +/** + * A matcher that matches any value. + * Can be used when we don't care about the value of a parameter. + */ +fun any() = object : ParameterMatcher { + override fun match(param: Any?) = true + override fun toString(): String = "any()" +} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.impl_AnalyticsOptInView_null_AnalyticsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.impl_AnalyticsOptInView_null_AnalyticsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en].png index 59d9ccc85d..211571ab7c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.impl_AnalyticsOptInView_null_AnalyticsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.impl_AnalyticsOptInView_null_AnalyticsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfd87b963e38343c0b9da618414b7bdc1fe79fbb3f48727916a1d20868eb7483 -size 50628 +oid sha256:1d5426156630c38e8222a52c7e78a30db19a5ebc3f39cfb671bef1d1bd7b9576 +size 97356 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.impl_AnalyticsOptInView_null_AnalyticsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.impl_AnalyticsOptInView_null_AnalyticsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en].png index fee4ff1799..d4e8907ab9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.impl_AnalyticsOptInView_null_AnalyticsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.impl_AnalyticsOptInView_null_AnalyticsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97f2610be9d2c2351dea5c7935e5d6bcb7213780280c506253ade9a16868a093 -size 49519 +oid sha256:6d8b5a13e95a7c9091275809337ad9b55058d4846d4d2ed5a2fffae8e1a0287d +size 91071 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_AddPeopleView_null_AddPeopleView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_AddPeopleView_null_AddPeopleView-Day-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c12833586d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_AddPeopleView_null_AddPeopleView-Day-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9828a78e660d9dfa53fcdcb5febfd45e2f112404e24ba8126e6a910254a064fc +size 45359 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_AddPeopleView_null_AddPeopleView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_AddPeopleView_null_AddPeopleView-Night-0_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2c66880825 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.addpeople_AddPeopleView_null_AddPeopleView-Night-0_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a7a6d9290e356382d127fa54d445a25fb5baea30689ded2999dfe199f593bb6 +size 43446 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..028a8e9204 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Day-2_3_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2fe3d47c7af259c25e0d640d60cdbd904b0c99a6e75ff773136d55b3986db9 +size 40818 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..62e97c8adb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.components_UserListView_null_UserListView-Night-2_4_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b7a41ea00505a0b6263ce4e6f906be24df830e83864a81b54baafa3a0434666 +size 39453 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Day-4_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Day-4_5_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f63426add5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Day-4_5_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b7aa4ae1aca7c541a0c8a7bc535f7ff98dd3b24127d0dcd9e76ef2a889e6ab4 +size 52249 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Night-4_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Night-4_6_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..70a363efb8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_CreateRoomRootView_null_CreateRoomRootView-Night-4_6_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:780070d15ef80d123a41f071dd5d13404cf65950944a94be364539607c3cba2a +size 49687 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en].png index 72a7b12e96..62cab21d5f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Day-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdc15ae26f55ce303a0eb02be9533d96f843bc0173560e487f601dd1a7c80670 -size 37180 +oid sha256:c769d9116f4173b7e97b9ecb73f14166e8ae21b968c13df779aaa4d4a12c7d8b +size 74024 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en].png index eeafd919fa..63d4e3e7d8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.notifications_NotificationsOptInView_null_NotificationsOptInView-Night-0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56b977a2c064e7753d8af2739055b89f19fd5031e5e7df92d348e8ec5e8f2c29 -size 33718 +oid sha256:2c9c2253982480ccf9558f2a1b6eaac478ddb3f7b178b39648871eb74aa877b1 +size 66490 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_WelcomeView_null_WelcomeView-Day-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_WelcomeView_null_WelcomeView-Day-1_2_null,NEXUS_5,1.0,en].png index ab74556c3d..b9b2597ddc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_WelcomeView_null_WelcomeView-Day-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_WelcomeView_null_WelcomeView-Day-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de05e9bffd5210895933179d1a331a768e61b7d398a26e93dfd8dc65bdd01c1f -size 286642 +oid sha256:93a96978e4575891ff2577c2ecca801cad9e95ddda4ff21e5c0d28f5ee256df6 +size 286706 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_WelcomeView_null_WelcomeView-Night-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_WelcomeView_null_WelcomeView-Night-1_3_null,NEXUS_5,1.0,en].png index 90be31dea2..72fa53cd51 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_WelcomeView_null_WelcomeView-Night-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_WelcomeView_null_WelcomeView-Night-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5fec377a61aa10b3f23eb28aeeec2871a7b08bf6da2e586b01aae153821fdf5 -size 390188 +oid sha256:994cbbdebe72e7385eb8c8ff98312510c0e8610a40dc204ce26e37acb36c0dd6 +size 390300 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_10,NEXUS_5,1.0,en].png index a2fa0ff4c9..758bb38311 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:654d73d145ba000b0a58e1974206bd9c7970a5a384186336469504cd35b19890 -size 29036 +oid sha256:f5aa4c9aab5727c56ee6efcf8c136cca8276d54695d28a4f548af9d51f3df4b9 +size 34264 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_2,NEXUS_5,1.0,en].png index 0cf654af98..8ac22522d0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32c2a61124bd68fc1921439f834744c1a40fa615de1270b8d97dd5c4365bbd25 -size 39317 +oid sha256:0d31d3549643b37a21f043f9ec9a33fa93e25f4004b077af713097855235effe +size 44276 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_3,NEXUS_5,1.0,en].png index 942a986658..f5be007193 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17c00b93a8073bdc2741f0492623ffef80d37e3f3fc8744650233d489b6efc82 -size 39866 +oid sha256:dfe460e8bab0a27a31984ce8c85ed98aef2a994584f724375cd436d5c1c7c6ca +size 44758 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_4,NEXUS_5,1.0,en].png index ca3fc4b9e2..8f629512ea 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bcd9a9db22a88210afa8ba3700abc1895d040c044ec11aa324d720651a3bc0b -size 40404 +oid sha256:e287869aa819ff569ab187f8995260410c47c32817caba61ba7639d6b0a2fe44 +size 45245 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_5,NEXUS_5,1.0,en].png index 8a6fc8596a..e5b5b23b08 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00804549fdbada3e48531ffab2bd5e013cd592d11c56e65eb2385b07910bc6e3 -size 39998 +oid sha256:1f6602e1e19e1c5b545522ff7444b294cc926d02b1910f196461a9770f64af4a +size 44882 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_6,NEXUS_5,1.0,en].png index 8f3fe73e50..9f81bd59e2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f71497d55a8cb048b3249221e8b24c883e3015533f4e543650b742542b5daac -size 40645 +oid sha256:4c321a39333a03b5eadf287e2a37e809da3620205d97caa1ae63f9ab99dc4f82 +size 45464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_7,NEXUS_5,1.0,en].png index c044d8db01..a9ba8ca80e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d4ad5e2fb1b518495f23b70fde606d791536adbea3ffac76c5fbb54e4c22138 -size 41343 +oid sha256:135f36df3ade2a4e210fc5560b8a00593e842153424da5efcd25b5c64036f59c +size 46240 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_8,NEXUS_5,1.0,en].png index 870e1cd2c6..2d2bf39bf1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:675bbd412179ad4960c6cf78f973c67fd11809340e414f7ebf3eef5e41434e0e -size 41201 +oid sha256:ea154dca2cde0829bb70dbcf9e032e63d052c35194b159dd72db8c72a58863b0 +size 46082 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_9,NEXUS_5,1.0,en].png index d8fffdef9a..97c322b6a9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Day-1_1_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33e82110711be0bdff074df90c93c3911e77bf53672df336c5fa6bf74f28d86f -size 28191 +oid sha256:f2124f3afde9e0fc4e42733d3624f69a74b898a035e47e4369acfad2834d0e02 +size 33378 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_10,NEXUS_5,1.0,en].png index 683a9a4755..0ce605591d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7220aa1b48506d58f800d71c0aa1b3ea1650fa2153246d1572213e5ff0170b04 -size 27759 +oid sha256:61da843a4fd576317ce1dab5a95cba47f269c96096157f880da5c6f3f8a944c7 +size 32148 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_2,NEXUS_5,1.0,en].png index 04e465574f..7e260976f9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36262a1ed8e2029519eb519e64dd973a5170269c04ad509464596b87bcbd9247 -size 37891 +oid sha256:0345399ddbe34e4fb13468571921fb2de8db1b7db01cb6a7439970b15046d37e +size 42361 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_3,NEXUS_5,1.0,en].png index c36ec3d999..0cbbb24fd4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c75a4422846ab1e4d12c48916a5f306b506cd1c082f1709104560634106a8ba -size 38420 +oid sha256:63b40ee6d9b4fa6f049dedbfc628380069360e67129b8e53b94e04d25b5897bf +size 42898 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_4,NEXUS_5,1.0,en].png index 08bc6aed16..02acc78564 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e28f6654d3fd5fb6e332a999fda8393bdff4988a87039e683cd94a251f4b9b07 -size 38926 +oid sha256:a5d9f66ab910df80652ef7658869ee05f570af134f1e05e8fc5b19d9a05a453e +size 43388 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_5,NEXUS_5,1.0,en].png index e5917eb1ae..38a46d5412 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a2cd0f46f405978dda374ce865bc5f409280716830fcd2d29e290c11b36f118 -size 38538 +oid sha256:e37056a6b4d6416a77af752d04b040d1fc8dbd65bdaaae8d50c05e7a11309d05 +size 43013 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_6,NEXUS_5,1.0,en].png index 490bfe9f87..0dd6058fd1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eac8803bd70cc677841f124eeb19e70f50a257cf89360410618db2c2f601c27a -size 39178 +oid sha256:995e4bfb172bbc09177d1427f63063f86348cedd5e497a5341eb0e8374e80cc9 +size 43634 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_7,NEXUS_5,1.0,en].png index 6bd5bbeda8..139f2d887a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96c95a7ca59ed0d8bba59e735a2248d2cee3d0a8d4c045a0491fe1731c8c9574 -size 39848 +oid sha256:1d086a8c7b3f9e6dbbf30495ba1d209b8436a8ef5d31dc9a3605a0b2cd9ce72e +size 44276 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_8,NEXUS_5,1.0,en].png index 3c897184fa..0058b2ffd3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a395ca5f5dbf4647ad1b5dea90f3c7a1cada2e934c4053f5ecc1e793e4efa879 -size 39718 +oid sha256:013dce6e34f67c62a514b958b502a511c9975551eb745b4e502c5794584e730a +size 44150 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_9,NEXUS_5,1.0,en].png index 1afb6e9532..6505ba7ac7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_SheetContent_null_SheetContent-Night-1_2_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bf2d4ecc1c9e67cd833ca5cd61b4f77e5da9381e4425f332b91e7fbf8d9d0f4 -size 26781 +oid sha256:cda82238c49b88bf0da5f462bcb05dee7fe2e659345c03997456be635ff4bae2 +size 31287 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,en].png index 273762d670..665c8811ac 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6293aceaed02886f3eb1d344c70f93772377d0ae09e69e6a382b33f577a26cd8 -size 14765 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,en].png index 273762d670..e247986272 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-56_56_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6293aceaed02886f3eb1d344c70f93772377d0ae09e69e6a382b33f577a26cd8 -size 14765 +oid sha256:703fe69f26ac8b3b7d199def97c4198bcf1e462b68fdc39924a865a54f298e47 +size 16524 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_0,NEXUS_5,1.0,en].png index 1276837323..fae8a6fca3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bc02697864bb7289bccfdfc137a7768b763c378e2ca057103058ae0d8c94f98 -size 13711 +oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 +size 4464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_1,NEXUS_5,1.0,en].png index 1276837323..b0f29f0321 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-56_57_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bc02697864bb7289bccfdfc137a7768b763c378e2ca057103058ae0d8c94f98 -size 13711 +oid sha256:7d1750522119e356ab0ee5697b16eddca04fe04f16c29b1d4f750756c5a59965 +size 14281 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index a8bf2184ef..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:455964d5b4d1652b5b9eeb8b7236f14c2c9490123399e8c65f1b9e920d20071d -size 21053 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index e6c83e61de..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-57_57_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:84d78e6672aaafd2c7974630e7d9391ee7e649c9a9de470627d1ed6e1241a1f4 -size 13366 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 098d9fe800..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c90e7168839f33c8ea4ef79308cc2b1c7a1ad4ec07dbeedba14dd080a3a841d8 -size 20169 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 302905149e..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-57_58_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0628f8cd42815e725efc8f7bfe0bc790c2369f8d4cabfd31aabd494a469f7379 -size 12867 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Day-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Day-4_5_null,NEXUS_5,1.0,en].png index 1ae62e8a37..80f3750a02 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Day-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Day-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:013bfb3d3bd78d8db9f0ac51b11a41c2b82daaa28f87cdfd866260d3fb40bf0d -size 23125 +oid sha256:5855e940f101e95ff1b0f01418240fc826543528e071193ede089b740d763b4e +size 23575 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Night-4_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Night-4_6_null,NEXUS_5,1.0,en].png index 243da80797..7cb778cd9c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Night-4_6_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerNotSelected_null_PollAnswerEndedWinnerNotSelected-Night-4_6_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c039ab6649ff160b540256dacba1037b15c5907d9994a041cbab3be0ee5cacd -size 21283 +oid sha256:651a5479a412f527fc1cae7b5b34ec7a53aa84ec06c97f64d84e0ee6dd5a03d4 +size 21734 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Day-5_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Day-5_6_null,NEXUS_5,1.0,en].png index e9142acb1d..67fc39d99b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Day-5_6_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Day-5_6_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a99ff857afe6478e579c96b984e1abdbf981638e6964a7d2c94b53153aff078d -size 23135 +oid sha256:8fc99783d3ecf3be99b2ae66d20072fcff29f04d5dc3c0ad59904421168559f2 +size 23582 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Night-5_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Night-5_7_null,NEXUS_5,1.0,en].png index f0115f6311..5b49dbe518 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Night-5_7_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollAnswerEndedWinnerSelected_null_PollAnswerEndedWinnerSelected-Night-5_7_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d97ba3288f5fcdaaed917a994df9273d3e82d5a6e0c7978044dbaf03152e3269 -size 21270 +oid sha256:1b4f569efd9c37fc699b2833e69794094d2f9e0188805192a0a13cb01a96d0e7 +size 21723 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Day-12_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Day-12_13_null,NEXUS_5,1.0,en].png index fec8ca5d89..5fc3514931 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Day-12_13_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Day-12_13_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2730173af19765a502b68278e709f441a1ddbfae88024b416029f7c87a21537 -size 49108 +oid sha256:6b4eb2a1fa6bd966b4031df5af47c019af6500a4e36c95495619d149b90fb8c6 +size 49376 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Night-12_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Night-12_14_null,NEXUS_5,1.0,en].png index dd786988f0..4ee4c80d4f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Night-12_14_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentCreatorEnded_null_PollContentCreatorEnded-Night-12_14_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d7cf73a11d38d17cd6ddbfd1819757a8f9db9db75da225ed0a9d9467644847e -size 45981 +oid sha256:9049f20df151c4ec18be7fd58813b00d2c647118e558eaa491e9aa71a9d882f6 +size 46313 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Day-9_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Day-9_10_null,NEXUS_5,1.0,en].png index fec8ca5d89..5fc3514931 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Day-9_10_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Day-9_10_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2730173af19765a502b68278e709f441a1ddbfae88024b416029f7c87a21537 -size 49108 +oid sha256:6b4eb2a1fa6bd966b4031df5af47c019af6500a4e36c95495619d149b90fb8c6 +size 49376 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Night-9_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Night-9_11_null,NEXUS_5,1.0,en].png index dd786988f0..4ee4c80d4f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Night-9_11_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api.pollcontent_PollContentEnded_null_PollContentEnded-Night-9_11_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d7cf73a11d38d17cd6ddbfd1819757a8f9db9db75da225ed0a9d9467644847e -size 45981 +oid sha256:9049f20df151c4ec18be7fd58813b00d2c647118e558eaa491e9aa71a9d882f6 +size 46313 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-7_8_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-8_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Day-7_8_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-8_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-7_9_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-8_10_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_DefaultNotificationSettingOption_null_DefaultNotificationSettingOption-Night-7_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-9_10_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Day-8_9_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-9_11_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_null_EditDefaultNotificationSettingView-Night-8_10_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,en].png deleted file mode 100644 index e39ff5329b..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Day-7_8_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f99b8193089b4719c75acdee6339cc89307fb16255cf6e7deaf4486b03c42505 -size 42728 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Night-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Night-7_9_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 3fb79982e8..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_InvalidNotificationSettingsView_null_InvalidNotificationSettingsView-Night-7_9_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ecc207ca338b66754c3d0e738f584c0712c8d46958e07bfd1ccd50b19df82006 -size 41413 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,en].png index e1135c1e76..8399a4c808 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cae06a603dd5947aa035e0be90a7a649abdce67f5cdcb921c2fa259ead90ac0d -size 60521 +oid sha256:0a566ec9dea61ea21c42cdbdb011f7b74a530d591642c5616038a0cdd6c20a74 +size 63452 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,en].png index cd66a12792..3abcdd8dfd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e860dc70a35f5fdecc69eec550e781f6974e66a5a12196572ea169b3422136be -size 54821 +oid sha256:c36ca0694f603d7a3f0ca1965f1b0fe2bb6738aece2a908317a8043b2cb82bc4 +size 57380 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,en].png index 742e797349..844ed57393 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f84e508a1a5ec9a402d4adbe5cbe963c0a06c776f7627a9c8eac29b6209c84a -size 54772 +oid sha256:dfd063ea23382a25ec6576b74674d6c2f30a7fbeb9e99d4c4a753a7909ece947 +size 57353 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1da5935157 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5ae68e5b9dcf951dbc7a77e6606daaa86eceee6b8b54c4fc5072774f149dae2 +size 47346 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f9e918b587 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Day-6_7_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:585f8dcbeef19fbba8e3cddcba7c8ec2831a704ba3fbb3cc05369d42444d919c +size 47975 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_0,NEXUS_5,1.0,en].png index a76c60413f..af460dc892 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22b0073e9e51a790231ab9feca750869e15b4c03e659f2b183dd6d5745be8918 -size 55839 +oid sha256:b9da4314613e463f2da12ac9e7d4c52446ca64133c13980b87a5c447873dfc5b +size 58568 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_1,NEXUS_5,1.0,en].png index 2484839eab..12405f0ce7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02b620889589dcd10057991dad95dd84dfddf9cef135059d1bae45ba4790f5ee -size 50971 +oid sha256:6eed8d6163471878f5281c5fc922537a22216e666366a4d1c6c11535c09aeb11 +size 53402 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_2,NEXUS_5,1.0,en].png index 0a1531e4d9..eba437daf5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:308f6f6d7da94c68212d263fa30f1612387d2e1dbf355314fb128e757f481209 -size 50382 +oid sha256:860cc7bbab5f72a951f7b83fad121040bd1fa1852bd44715a390b8e5db52eaeb +size 52801 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4ecedce7bd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4eacf4ac22f6c49b96f0891bf53fcc96bd333a6b42e66c7ac12aa4be03232667 +size 45141 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ac4c51e310 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_NotificationSettingsView_null_NotificationSettingsView-Night-6_8_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6f6367b13643ae480f3d555ea3506bc4caeddeb51c0cd20de6c906b9886e5a5 +size 45015 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png index d4df69f743..c54c15e9ca 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be4d708775d03680be450dfd5eebfeda24f1841eedced3e7de2ba036669ac479 -size 39086 +oid sha256:75fb611e02345fe1cf7947d1e160e309cf8520f0256ebd25b43bab8c3102f3f8 +size 37138 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png index 9d950f5916..90e78c16a5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewDark_null_PreferencesRootViewDark--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53660f4354d2a750ab539370151c9ee4ac49f9d06712f7646fdd487bf133753a -size 38766 +oid sha256:a7be4df5a4e391d2253f283acf0fb9362b965fa887235221b794915d74891899 +size 36783 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png index d8b913deea..4f2683cf78 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ae1828957e00adcbdbb772a0ebdc1fb6604afbe644cc5d8662da9fcd3973129 -size 41162 +oid sha256:67503a61f560ef79d7e981057094dccf46eda167ee84427c1aceb9f83edf0146 +size 39080 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png index 7f24f64da5..12e2374878 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_PreferencesRootViewLight_null_PreferencesRootViewLight--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58e567815b4dc36abf8422b17f9902d8581dbec1a1ed216df2f120c6198b6fe5 -size 41124 +oid sha256:5cb3be70de8e3878bdd90db9945aed60fb2b38d8c66ea610806226c21b1f5a5a +size 39052 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-11_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-10_11_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-11_12_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Day-10_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-11_13_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-10_12_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-11_13_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_EditUserProfileView_null_EditUserProfileView-Night-10_12_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-10_11_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Day-9_10_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-10_12_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_UserPreferences_null_UserPreferences-Night-9_11_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_0,NEXUS_5,1.0,en].png index 364d5ad96e..91d3f82d6a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cf713ae66135179f17b52578bf4663bf34a102b92c0809c42e5285856d43f3b -size 18257 +oid sha256:f9eb707bd20836dd80d1939a1eb405bbeca4b58aa36e9b627f3775cdffc9dcc0 +size 15142 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_1,NEXUS_5,1.0,en].png index 6705985509..fd7266592b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7737c502737b811720b54ba8e8db4248df8e0e8577435c61270853f92e65f6bc -size 19731 +oid sha256:56873f51dd8de6630f5423f796687049a920bddafb5af17a47e4f901a5d9f360 +size 71767 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_10,NEXUS_5,1.0,en].png index 8e48dd4ae9..bb978036ab 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b0ca2b91bc525254ed4be526e622608f77d809a1c097ef02943d4cd751c65b2 -size 55941 +oid sha256:34efa87358c4529d0f67729fd417a3d52dc5a0031192dbf123bcc667d6e78391 +size 56803 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_2,NEXUS_5,1.0,en].png index 205b4fe623..05468c5c69 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e96824d27d0fda0b183e8c90d659406d012861ff520f81cfa60374424137bbdb -size 66079 +oid sha256:4818ed45a985359112ee92c38148f1030647ead270727525fed1cf1f5c493180 +size 66293 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_3,NEXUS_5,1.0,en].png index d24aa84010..35c6953ca9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b00d18ba4fa132e32c6580682c6b78d754d330735afcf8e4242ee0def213ba44 -size 66046 +oid sha256:57b0cd541704cd82962e03d2173bb37350d2f91bf1f3d6121314bdccad50c8e6 +size 66255 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_4,NEXUS_5,1.0,en].png index 0eb57ff9d0..69680bab61 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeabb0a4ed645c7f47da62e7bafaca54ab1d3c5c48a195b48c3f807e5137d39c -size 59992 +oid sha256:ebd6484e81803758298b4ff0e19a042918d85c29e3828a561c83328a9b3e4d73 +size 60644 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_5,NEXUS_5,1.0,en].png index 40bc944f5a..7bf27add4a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5199d1491a315d7653b5e09049ead901650332deb50ec4d24742ac7f3f7ff535 -size 15618 +oid sha256:5c78aba6e190dd73671503b4fa725c04c9bf23ffc3b7ca953670663e16fda7b7 +size 14607 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_6,NEXUS_5,1.0,en].png index 11c7296257..f5a3dad0cb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54461c606debb6867bd48a054f551dd56e2895530f06e6890b44ad7b1eeb9219 -size 65289 +oid sha256:77bca4e3380c63dba4e7def0937770c6d5b1ae5bde61e4743ecde035431fb8be +size 64625 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_7,NEXUS_5,1.0,en].png index b5a534c0d2..ac13b7852a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66707e11931cc5824142a5f42ed95c4becd5621ea3a9c9cac3df5f0435b3883b -size 66191 +oid sha256:b265ee7f4eb6c56e531004af6daae9ad89167b027d089fa960fc17553ec80131 +size 66783 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_8,NEXUS_5,1.0,en].png index d74825b1b7..996e749c1c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31dcb5cef9f857e2b96631b4dd23a59a200c9174f8cc0e406d2a859497a8f893 -size 58934 +oid sha256:eb629e088198030a3a9a09c8cfc815a87e831dc6f5e26b225d17c61883585e50 +size 58902 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_9,NEXUS_5,1.0,en].png index 7f712e5d7d..0f822efc96 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Day-9_10_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56e0b337fc6afdadf477785df479478ae24dd7c9cb1f98bedf33b920ad374e78 -size 69899 +oid sha256:f4dd6cbcd17c5c97c240a1244f3233f0f228612b317cc8f6b3f39a0d0d2b05fd +size 70120 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_0,NEXUS_5,1.0,en].png index 9c99ff605b..95f73d3ca5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a99d564c528dbcc2080b6f83ca09a97bd518412019d0f32b76b80086af4f17e -size 16966 +oid sha256:4e47b753ec51c5cdc4640b9883bd9bbcb68af0c0ceacbc31020132213fb9bfbc +size 14223 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_1,NEXUS_5,1.0,en].png index 5e3ca5e603..eb5a850bea 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb3c08d58fd0dfa60ad1549b26a53f02239f1ca43aeda9e52350952d86d5fa74 -size 18178 +oid sha256:9d1cf36a65fc7e916a15a0a6eab4e5eaba47f8df5d39d5cb9fe75b137138a96d +size 69609 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_10,NEXUS_5,1.0,en].png index 9cd1a27f5a..c34a2b7cf5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f86fad7b780fb7fc0b36377724c6830d86c5d41dcfd50f8de8cbe9f40473e68d -size 52719 +oid sha256:fa0b65d6cd2eac2746a2ba1598f075a6d5d144cfc62fbde8e7e17c5b69d85f1c +size 53497 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_2,NEXUS_5,1.0,en].png index afbfa647bf..37e994e196 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00c4dba57e32a87d6acde138625214bb795c7e7c8987561d6ddd53fbf26674b5 -size 64446 +oid sha256:417ca17376f6e296a8955390c800191ebdccf8b358dbc738f438b70c2568606e +size 64439 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_3,NEXUS_5,1.0,en].png index cc77b40b16..a3490db6e3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3e270adac07c800c5ab1a124d564beee96b2e39e9473a69b8b90ff647bfd6df -size 64184 +oid sha256:a73ae445a25011b1c376f93b7e7203b023e23a027aa2a44d109008a1cca98406 +size 64193 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_4,NEXUS_5,1.0,en].png index 7615e4d3de..34fda705fa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6490597e92bd937451dbcf7d516f43525b5d18835cefec4f825fdf90ee6dc626 -size 57891 +oid sha256:a98762c06334da0cc53d04826623850b15d3fc5fdf7cdf8f50f896574a048d0d +size 58356 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_5,NEXUS_5,1.0,en].png index d0d83c7d16..4bb4ab76e5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e088b944b00b462fcbaa16033a786835b8a09dd26f8a9b946580a237d2b363e -size 15064 +oid sha256:b96e9765dc77cb7a16da3d241b9ee90a7039a5184bf90002b2856aa9a695b446 +size 14304 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_6,NEXUS_5,1.0,en].png index 4eaec1e29e..9f39eacc46 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afac0f9958320d5c1f12d9b6647a15608eff51627dcb82d35ec9524a579f2637 -size 61111 +oid sha256:b6a02199d5c7b5bf4ff0251d437f071e2ce54440b1e0b4519305e0080f7ed6ec +size 60375 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_7,NEXUS_5,1.0,en].png index aad0cbf857..85da1ba694 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3991def6f61df49c73e025a417680f1bd409d8e2aff41ba2434e3a5a50914665 -size 62128 +oid sha256:5898156b9c4ce053ab24728b0679bf6d2d0873459a7b1e4951f8f9ce646e4074 +size 62587 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_8,NEXUS_5,1.0,en].png index 247ab0e9a6..b06975b046 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af3c14da68316feb82b4ae905868116239e43cb8b0e5393246efe0edd61f1bc2 -size 56497 +oid sha256:6648e7dc5c97e27efbf347380fc3ae3c67866595b086e1adefe36a274bf3afc2 +size 56314 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_9,NEXUS_5,1.0,en].png index ad76d6153e..cf1ca92dc6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_null_ChangeRolesView-Night-9_11_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1979b5b0dff66a05765a1ecd03d9a896d2aaf6dffc686ffd27bba3d5a0ac927a -size 67254 +oid sha256:5d984c253b4990220acebfe91d33fb7f4201fb7a976bea48b2bc04e4788ddee2 +size 67263 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_null_PendingMemberRowWithLongName-Day-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_null_PendingMemberRowWithLongName-Day-10_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7f2ece3429 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_null_PendingMemberRowWithLongName-Day-10_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31deb74551ce3e14d7fdbb6fad4bd1dd9c4124d50a39dcef572c1092eac75561 +size 16580 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_null_PendingMemberRowWithLongName-Night-10_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_null_PendingMemberRowWithLongName-Night-10_12_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f33d0577c4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_null_PendingMemberRowWithLongName-Night-10_12_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e091e28939405c49558f40dbd291ace6280152b84e10676188b51f0ddc5a662d +size 16104 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index c1fa064495..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cce394023e7c5bac40bcf234f9073196b1eb424893760f29d609eb329f62817f -size 42587 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 8428ee7714..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d6818023d37493825ba7b929470152847c397b5f19a5bd8ee862fc900dee690 -size 40755 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 52135a6c75..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:86aa47d55841e5041bf56534852dd716b4546cc3657a30133b88c0c87998f16c -size 42464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index d7bdf55419..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:237125629b6449d6898391235b625248b71629b4d96415b80fb7ef5e773e86b2 -size 40484 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index 6c7a7c2b33..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:753eecb930bf0659671f2f1ecb2a64ae226aac96c5365ed549c635fd90c8c729 -size 40951 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,en].png deleted file mode 100644 index bca446759d..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_6,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0b37566f62e30fcb4f16364a9ac2c008a98a5429c3fa5724d7ef6455e79cf34 -size 47995 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..25e2ed257b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:491c4c2465dd79a98e0062a7dbae9dffb1dca66ac991cacae44bc31fbd415b1d +size 42557 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-10_11_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0d7ebc9df2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:373b21c8f450fcdde73431e55fad3b8f7f255ca3f5d954e9555b65ca9d891e4a +size 40720 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ee3dc1981a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b9b89615f55b7f33c4d16c2affaf0ad768c2dc940dc512c22a3376c7a70266c +size 42434 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3c662c7fd9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a54f44acb3270d7822cbd3e1a4ff92dbc15e3efb01d84b4b86157820e68b8304 +size 40492 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bcbd74311f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:592b7775769f8a51bd3a2a3a4d81fe5e2cc95de9d781b446a74cfb39bd53aa0b +size 40954 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1e8a0d2813 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Day-11_12_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eee33a3534c66fb6a6ecebfdfe3518a7446880905da5bb0aae11ff260f7470eb +size 47999 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 35d6c4ad87..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dff4c4a80156b2977da007d6258d1cd6cefc93bc709ec33bff0e1b484604cf0a -size 38506 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 97de47b574..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af5a843cc9cd5a51d986ba7dae7d2c6a088f862f46bac961667a1fb1d62b8a20 -size 36966 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index e7363aa84b..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:059e598dc973e8c01dba4ef49accf47ea2bd74377dfbfb441c3c213fc77a1cee -size 38158 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index 7f5fe9dbc4..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c34415877b279b2018d02667cc98b4857f4ae5bfa68d169f3226a0c99ed4620b -size 36699 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index 17ca2569d6..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dc02c16667b78ca5a9985da6816f17bbd0f7bb2307569aacaf3391ccd8431bd5 -size 36567 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_6,NEXUS_5,1.0,en].png deleted file mode 100644 index 9d7aa5c1be..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_6,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4faece23a4780f5465abe42587f56933ecded55bf3ae966a9e6b634e7df1e049 -size 42845 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..101d42b4bc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be6fc53e78d78c117a1e166fdd3e618eb3e2a44ef3effb74f9aa0a2116a0fec3 +size 38484 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-10_12_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d8a11cb617 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56c2f01102a7e2902981a4a71efaf838ffe0fd1efd0c4a1f42afbdac013c6f3c +size 36940 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a031323cc9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:540804fa0abf2754e55616fefc03991d692a464bb4679b110a0a7bfc99754338 +size 38135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d5c54c295e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9759cca46c3158225f780439f7da527b5a76c75a201b10f6ba50ccc2022a9f1b +size 36664 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e4539c272a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f4eb202f7cffe9eb5dd51ee32abccc5e97877c6d35df82b6d8632fea611370f +size 36530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..41b61c015c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_null_ChangeRoomPermissionsView-Night-11_13_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d332b41b0a566848e81376e335706e822a25cbd3c40233cedef3f21eba7dbf0 +size 42811 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bd44675e52 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b6110eb2eb66c404f787472dd18608a27a3c8ce19c794ed4b3f67e186720978 +size 13703 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8d0538f792 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2b6cc7995880af654439c4f45d6e3ff35ff9c92609ad136fcdfbb2050edea44 +size 30484 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..158c2f7cf5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f7885c0ee25d12dc5bb25940b35e7153f95ffc35cbe6fac21839390d52faeac +size 32326 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c77504c186 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:776d8f8a15bac930896a8baaf46f69fddf24fdf5c51c1ab6f1bbc48ef89259d6 +size 31607 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0eb807db6d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Day-0_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b349aa0ce78db6396094adda401f52133c32c16e695a051ba115be0143c31de9 +size 34972 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ab56f08348 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9eafd587065c22d19814285b63e7690f5ff4bd1ed0765bd9802e61d6d8ed0ba7 +size 12915 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1ee85c5f13 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a7b5c11dbc45a804d34a0105c49c7bd15ab1f5d6441f0e95dba84fd8a9eada6 +size 29080 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..80d7eb5d17 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6af6d92766e7249ee95582a32ac17ddd5c3bcc29c9b78d9ba9ca6bcb27b69c8c +size 30828 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..34fa07061a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b434b666666da81963d8ac89e9669f059443cd529103735944e6a15ba5ba46b5 +size 28646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cbf54de729 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdirectory.impl.root_RoomDirectoryView_null_RoomDirectoryView-Night-0_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0190f4e7d06f5b46147531d7224e24662e0a7ef6289d516cf082d59ab63627f +size 31342 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-8_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Day-7_8_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-8_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-7_9_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-8_10_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_null_DefaultRoomListTopBarWithIndicator-Night-7_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-7_8_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Day-6_7_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-6_8_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-7_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_DefaultRoomListTopBar_null_DefaultRoomListTopBar-Night-6_8_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Day-5_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Day-5_6_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 1ff9a4e140..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Day-5_6_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f5fd867f51badedc8e9ded0900eb904ac11af348243573dcf34b8aaa3c1548b7 -size 26926 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Night-5_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Night-5_7_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 604e02b79d..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RequestVerificationHeader_null_RequestVerificationHeader-Night-5_7_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:318838247c7d0cee133fd5c98bbbee6f2c5be09a95b6c2fd293c041e21480ab8 -size 26080 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-6_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Day-5_6_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-6_8_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomListContentView_null_RoomListContentView-Night-5_7_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Day-9_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Day-8_9_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Day-9_10_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Day-8_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Night-9_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Night-8_10_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Night-9_11_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryPlaceholderRow_null_RoomSummaryPlaceholderRow-Night-8_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_12,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_13,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_14,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_15,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_16,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_16,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_16,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_17,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_17,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_17,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_18,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_18,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_18,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_19,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_19,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_19,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_20,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_20,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_20,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_21,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_21,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_21,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_22,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_22,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_22,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_22,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_23,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_23,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_23,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_23,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_24,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_24,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_24,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_24,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_25,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_25,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_25,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_25,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_26,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_26,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_26,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_26,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_27,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_27,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_27,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_27,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-10_11_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Day-9_10_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_12,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_13,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_14,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_15,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_16,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_16,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_16,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_17,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_17,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_17,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_18,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_18,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_18,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_19,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_19,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_19,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_20,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_20,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_20,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_21,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_21,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_21,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_22,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_22,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_22,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_22,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_23,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_23,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_23,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_23,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_24,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_24,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_24,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_24,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_25,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_25,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_25,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_25,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_26,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_26,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_26,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_26,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_27,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_27,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_27,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_27,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-10_12_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_RoomSummaryRow_null_RoomSummaryRow-Night-9_11_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-11_12_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Day-10_11_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-11_13_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-11_13_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-11_13_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-11_13_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.filters_RoomListFiltersView_null_RoomListFiltersView-Night-10_12_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-12_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-12_13_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Day-11_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-12_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-11_13_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-12_14_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.migration_MigrationView_null_MigrationView-Night-11_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..872a2f2274 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:615e00dfa47d40fd459d268d6e68c61a501be7e4a24bfe7d18f3a647da8c5f08 +size 10595 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-13_14_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Day-12_13_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..845fd9fc0a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:421e47de6c2bc7fd0a5de84b6db4bea79c7312442244ec11506164150762943b +size 9787 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-13_15_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_RoomListSearchResultContent_null_RoomListSearchResultContent-Night-12_14_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en].png index 761cb8d3a8..1ff067e54b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8de26fbd49153871bdd18defc2297b2ab314f541fe598894a9c81fe633488db7 -size 51359 +oid sha256:0197c643bbbbc46dbb58ac8911b61003bb09fceb15f1997cba64bcb00aee90fe +size 162642 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png index 1ff067e54b..3057c59ef7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0197c643bbbbc46dbb58ac8911b61003bb09fceb15f1997cba64bcb00aee90fe -size 162642 +oid sha256:af42c9891a6670cdde07cf054e139ce1f83e877bb68f60463fcad6dd28d8e049 +size 6867 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en].png index 3057c59ef7..19b6010d17 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af42c9891a6670cdde07cf054e139ce1f83e877bb68f60463fcad6dd28d8e049 -size 6867 +oid sha256:46e28da384551af27bf9c52a45c17b8441f8875494b3d3fd901ad66f5d7ff89b +size 74803 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en].png deleted file mode 100644 index 19b6010d17..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_13,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:46e28da384551af27bf9c52a45c17b8441f8875494b3d3fd901ad66f5d7ff89b -size 74803 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png index a5a84223bb..872429b353 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ecdd0cf6f38c2be14eaad9152d7f0eac02682d12c10703abc605bcc73adbb92 -size 86084 +oid sha256:3f314f241b95aeaccd576c8235cfd907abaf5d796b5dc6f4e2acac8a4b9c7c2d +size 88678 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png index 872429b353..6efd101dac 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f314f241b95aeaccd576c8235cfd907abaf5d796b5dc6f4e2acac8a4b9c7c2d -size 88678 +oid sha256:a0037f0401b53088f2a75af67f8e4df374866fbce80c052535af1d97354c9b22 +size 56605 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en].png index 6efd101dac..761cb8d3a8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0037f0401b53088f2a75af67f8e4df374866fbce80c052535af1d97354c9b22 -size 56605 +oid sha256:8de26fbd49153871bdd18defc2297b2ab314f541fe598894a9c81fe633488db7 +size 51359 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en].png index 168ee31a4d..043a1223a4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35f35f058cff4435c278ba8cb9f60d68edabcc5da3ab58823abf99a082346978 -size 53292 +oid sha256:4b167704c2d05c857715aa11c3d7fb3abdf3b59ad7d6d2c479936d934a67f32d +size 186756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png index 043a1223a4..08cfca5c0b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b167704c2d05c857715aa11c3d7fb3abdf3b59ad7d6d2c479936d934a67f32d -size 186756 +oid sha256:6e95dac6e75d3f615ccf4d8e98f1780d8aeace1cddf0b33bba8b485860d3216b +size 6688 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en].png index 08cfca5c0b..52eccc7865 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e95dac6e75d3f615ccf4d8e98f1780d8aeace1cddf0b33bba8b485860d3216b -size 6688 +oid sha256:58ce650742396cd4addf9f545b2488bc2666e848b361a501d3ea2508c92c2a8a +size 76908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en].png deleted file mode 100644 index 52eccc7865..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_13,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58ce650742396cd4addf9f545b2488bc2666e848b361a501d3ea2508c92c2a8a -size 76908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png index 21f5c4f252..2728c83aa1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:308e785ea0c43cafd2daea869ecad96cb2e4baeffd747148f1c2f853604603c8 -size 88246 +oid sha256:59c4e7f70dbef1f7c95edc705729dc37f7be3e96f48264e5b63c51f92c13aa22 +size 90762 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png index 2728c83aa1..9b8391501a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59c4e7f70dbef1f7c95edc705729dc37f7be3e96f48264e5b63c51f92c13aa22 -size 90762 +oid sha256:528d43fa1989f1368c33f4f447347caa2fefb92c3a0a1e48846974ad96f262de +size 58510 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en].png index 9b8391501a..168ee31a4d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:528d43fa1989f1368c33f4f447347caa2fefb92c3a0a1e48846974ad96f262de -size 58510 +oid sha256:35f35f058cff4435c278ba8cb9f60d68edabcc5da3ab58823abf99a082346978 +size 53292 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ecac9d32ec --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Day-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e7892eb99a1307f2c34968f76e994e6b8e3916ff5c218eb9cbc81424001fd54 +size 64251 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6be4090def --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.createkey_CreateNewRecoveryKeyView_null_CreateNewRecoveryKeyView-Night-0_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbcf18bb210a68b0e02512abd949845a8c5a705a177ce0875599734ce46da7ff +size 57525 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-0_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-0_1_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-0_1_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-0_1_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Day-1_2_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-0_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-0_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-0_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-0_2_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_SecureBackupDisableView_null_SecureBackupDisableView-Night-1_3_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-1_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-1_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Day-2_3_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-1_3_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-1_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-1_3_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-1_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-1_3_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_SecureBackupEnableView_null_SecureBackupEnableView-Night-2_4_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index c149b61eba..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92f023f5edc4c050932f8a9e3a3f886285ef9cc716e999cc66f32101a58f295d -size 33356 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 5d53c122c6..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:380e3d8df049edf96053533d8d22b8bd46215a6961c6d2b6ca6d151b47c2201e -size 45361 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 3a7c3ae48c..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:713d30ae1e8896a410396335ad72dbf75d207096a59caf90e6d3135d4cd55254 -size 43747 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 7e4da73521..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d3f640e29d7029a278172b45e0597e45653beb58f195bcf71f3fc31c8832708b -size 42973 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a062477882 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6df37aa3a9d8411d3b3f9cd4dc67e48c271030350fee5cc464da8b564bea14d0 +size 40410 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..60aa757d1b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:923a6792ecb57eaefb86042327495ffa48294187eb97fdb2f36ca16b8e2d2859 +size 52163 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..43f66fe1bd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9296a3f7d3c3ee8aa4d8ee3fc310cab8c539c7adfd4b14156da65608a5e6fd10 +size 50565 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fa793735c5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-3_4_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fee5009acc5128d48791066fb45555d0fa261cf09c9b372cca8570561c3055b +size 44297 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 11bf947e5e..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58529ffb570bf24dffc2bbc019de2d108467d577ce5b699a09ab62ee22882bdc -size 31854 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index c1f40a5686..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:933b83af0d608e39b8d97a3bedee82040a5a13889e23dce7802955488e0fe227 -size 42290 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 47008a7b67..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:62ae02108b8bf661d7f799b64d5e6c1794acb00f1c91a50086fd279fe6ea62d4 -size 40908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 52594d6e32..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:24285b1233ff3695d940ee3025b45e83a2b6094f17c9c6b65e8f1bbdccc7b7c8 -size 37961 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1855caae90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b11e7ce69453d49bb916bec23ce0974e33ae788911ab12cea777ba238141bc77 +size 37965 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..68beb560ea --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aefbf69085a8279090018dd35a465614b2c876932746e650e7faf6d84f062381 +size 48364 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..65c93af2ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b6c6e849d68d516000644352c347587910f99ffd0d441feb8cf72a981aa30e3 +size 46849 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..26d115b0d0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-3_5_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65c1faa6875a6c2b9ebe282f83e8a82b6e358f724082b53525d31a6c1bff4b15 +size 39312 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-4_5_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-4_6_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_10,NEXUS_5,1.0,en].png deleted file mode 100644 index 61e0ff549c..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_10,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b30edf234beec3069a3b0ad051f3c166cd0b9d266491a0dcf5f62a1f5c4ce3fb -size 24123 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_11,NEXUS_5,1.0,en].png deleted file mode 100644 index bbc5691f3f..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_11,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cd8bf20f556a3fe2decdbb5e1f471ed9de9545d39ef5651c5c7b915d3562210f -size 22167 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_8,NEXUS_5,1.0,en].png deleted file mode 100644 index 4743f2e9d8..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_8,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:340de66e7f38fc545a0cd153c26c8f46c085fef9a6cd3ea51d63d7b4da74d498 -size 12690 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_9,NEXUS_5,1.0,en].png deleted file mode 100644 index cb977e1604..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_9,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:94181e4ad63d57a52d5726440268a2e1c85d8a653c44fd01cc3750f2d2ee2802 -size 12491 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9cb0608473 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:513dcbe90f37494156e6d25c3aa37577f2028e57baf8efc8c8f0877e4560f247 +size 28759 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_11,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c92923ee62 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_11,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9062b0003b8d53b3cf5f3ad704f98397f195e51e12ad1c4ff942ba9660c2191c +size 27150 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-6_7_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..64b5b8c5ff --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec184cdcf450def60da5f1598a4fe1e6974dad5e57718db531f996dad7fad50c +size 18556 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a0e9ae8aa3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Day-7_8_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc55f58f8a0924f910d32e16d3a7147a1d28a9c40628f3906c6f21013771792b +size 18354 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_10,NEXUS_5,1.0,en].png deleted file mode 100644 index a3f80cbf24..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_10,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:192a4633b725c9bc24c1782cecb4f4e7bf1266372af7ebacf64637aca943a260 -size 22553 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_11,NEXUS_5,1.0,en].png deleted file mode 100644 index 3937240dd6..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_11,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce05cc46725f3f283067284b79c7187ec1f6491618d29636b37e55eef4ce39d2 -size 20567 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_8,NEXUS_5,1.0,en].png deleted file mode 100644 index 106212d72b..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_8,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f5f8a85ddac57eec6d51a0f51188ab0111686982d94769cdcd378653417b41f1 -size 12303 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_9,NEXUS_5,1.0,en].png deleted file mode 100644 index 2fc240d96f..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_9,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:37ee8dbcecc2ee4c3fd10e5086b4f2833eb878cf9beb09515527e8657a56a55e -size 12019 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e93df38559 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fee98d6b4239b51814932af659fbd4e5ea8316c60ea94788c08f6718e36da135 +size 27212 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_11,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5535e1fa78 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_11,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a12e263b94ec603fb10e710fcfdd678d7b88a4fcc49d6db0a85d06ce1aadc85 +size 25444 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-6_8_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..750f1ab968 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:659444a228416eb4b38f9bcfec8fbf140bfbdac62ff681dd1d74d228149faa41 +size 17691 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ea7c22b95d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_RecoveryKeyView_null_RecoveryKeyView-Night-7_9_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8569650805e76bc73ec054459096016b18afbe161abfa50403707e2497906fa +size 17407 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-5_6_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Day-6_7_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-5_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupViewChange_null_SecureBackupSetupViewChange-Night-6_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-4_5_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Day-5_6_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-4_6_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_SecureBackupSetupView_null_SecureBackupSetupView-Night-5_7_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_SignedOutView_null_SignedOutView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_SignedOutView_null_SignedOutView-Day-0_1_null_0,NEXUS_5,1.0,en].png index fd4d7fc97b..f145a1f1b2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_SignedOutView_null_SignedOutView-Day-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_SignedOutView_null_SignedOutView-Day-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bbb1f43f85ef3ac2c24182ad91629e5779d0e812d8fb2714f3c497bd6fca1cd -size 60462 +oid sha256:67c0ab8e3553db828c81ea56b2397b3a3f9a2fe8ff6f668ecdc460ecfadb4726 +size 60375 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_SignedOutView_null_SignedOutView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_SignedOutView_null_SignedOutView-Night-0_2_null_0,NEXUS_5,1.0,en].png index 9817a5e985..105c9840f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_SignedOutView_null_SignedOutView-Night-0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.signedout.impl_SignedOutView_null_SignedOutView-Night-0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b452dd45e6581d3661724eb537d4020b73cdef47775a04d26e28d449f3d8a9b -size 58730 +oid sha256:f1b0d0d4cb8eafdd9eafa64f4788f32052542036757698200ef16ff0e20562c5 +size 58668 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en].png index 25be545a89..5fe4883b6f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e5c225258a6b257afcfc935971df619199a8985ad87203528d27a166a6fda2a -size 28334 +oid sha256:3fedeb54cfb4b08b3a75dc528b518966938ac9504471741cd9c0bcb90bab08f1 +size 28636 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en].png index abbb0fefb9..d22bcffed1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c13a37dfd42c7e3872cb7854b5f4b5b03e24e40f1751a8a3c430376cab8a0669 -size 27203 +oid sha256:761bb174a847aa9fd33f3bbba08473d36fb8319259dbef89d6f907581600042a +size 25323 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en].png index f239a76249..431dc18371 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4545536311f20615a2809a067af9eabf1a9398f39a9df819dd3c0a4c3de64bd -size 51321 +oid sha256:c5cd4953883ddc280d12986db679383d69f9fd491e25e6719a309d0e891d6f6b +size 50793 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,en].png index 4e7f01ae48..d1d24b44f5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26759996939952786a94b9d860e02c40154c0903a49afb8e7be05a20787f0fa4 -size 52615 +oid sha256:50d3fb9d04f37ef7b78c6f2736d627da26e137317720e9b1de8d773e3ca12dd6 +size 52382 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,en].png index 22ffbb29ae..5a77351c06 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ce368edd4536137fb4545fb21892c51af43d7325f21b6e34e5ac1a72b818b34 -size 31575 +oid sha256:909c750c58d01cff00802db2218c52fc7c13f3733996b6c4e5d204daf464ae1e +size 30718 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,en].png index 7dec5258da..45b14790fc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4bff39ac46298be6b968326b64c632aa73b13a51f1efff512d5c6894c414a1b -size 20951 +oid sha256:2994892e230bf79fd9dc6eed9c21854c4ee441fef1b87e65207504de125c0e74 +size 21847 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,en].png index 7908dfeabd..0559f67af1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0843965afef3d142709f377ab408866cacfb016f652107ee84dc2fd39770ff17 -size 35490 +oid sha256:0dc7d7114d4365aa6f6a5fd645fd448e6088b2fd618efa49543eb2dbe0a1b394 +size 34779 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png index 609952770e..042131d574 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cbaaf6ee91626a9e8db83ce42be14e1a02a8c841230bf7829fa74aff8cbdfab -size 32513 +oid sha256:92d98ccd5cf7f64cdeb531f0bc4f48589688661a6338de0628c2815b12e59d86 +size 27456 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..eabba435c0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:409858802c14fd755380aa67fedb52f04a5668cf0999104f4fff4ffbc3711743 +size 28968 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9bef6f55d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37e571dda745d7d71df2d421651fbed56a8e698583b6b2684633574ffc8c56a9 +size 28880 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en].png index 0795fad021..b3cddf5860 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ef7b72a829b4add1f21b5f7fc06d6a260227b9c065c08ad770d70656b28f815 -size 26848 +oid sha256:abecf71fc305a7fd44f791ca72072f34601a49dcd1eafd4fbfa34fa58cf3db2a +size 26800 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en].png index 54d46147c0..2d08d6b799 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:383436d7acb3474d2d217bce7e566f9ca2957324fca95eb2ffb93fa1b2038082 -size 26117 +oid sha256:55decf810e2d313554710cab91decd8dbd11ee2d18eeeec760f327b0bcc24063 +size 23507 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en].png index 128acd813b..f3c45f52d3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fdcab5fa8620cb0ba0cc0cb822fe4a4807198dbbcd7de7dd7a230d2fe3030e0 -size 49141 +oid sha256:4d5458b28ad6ba716752234a8439b72fed9133c1e4b2b3fa3df2fe87f03df6ae +size 48369 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_3,NEXUS_5,1.0,en].png index c8329b273e..cd697f8dbf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06b8e6bce1573d8e39a1bf5823945733152fde066a394a3e90f2bf8dc7929ea3 -size 50526 +oid sha256:c7662be2a6450384daa9e3d92c15891e53043b3181c026f4051443fb62334c96 +size 49923 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_4,NEXUS_5,1.0,en].png index 2db49fef25..472438841e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10c16c9a29f898a6a33f8bfa10df24d2da9adb93eeb764e2b744c6efe531890e -size 30007 +oid sha256:de24ec97169e51b1d820e2eccdd283e7cc666635159384d8a6e62704b168ed6f +size 29404 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_5,NEXUS_5,1.0,en].png index 29ac2f3227..e38d40d6fb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4786a280d694ead76992dfa75f09bd2e20c292376509f86462a359d19be3e50 -size 19984 +oid sha256:23009d617608b10fe8a4ad6363c0a4feebb49ff160c0bd4c308eda1b2fc258a2 +size 20608 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_6,NEXUS_5,1.0,en].png index b2be2a2397..59ee74b726 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59cabf858188557db3f2660f71cafc9b041977c656693dc5bc009da71a5c7648 -size 33457 +oid sha256:971e3b14de6ed22d1c15687fdfe3bd2a56b5665ac250ae868ed7afe427778d1f +size 32510 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png index cb997ffa7d..1abc88027b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efcf672eba104371f27b43c9204d207f11f9ce42098da0e4dce3da4aa6639153 -size 30517 +oid sha256:5ab2e5a17ffb9ae5fa5b62a9562920281af9e2d9d2c51b80fae4c973497d118a +size 25545 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..312fe2c859 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7eb330ff5a506ee6198a0ec5acc3ef1b9db58161aa8eb9dedf468719f3e55a6 +size 26778 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2b9295e65e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c39cda078df3c3ac3297c86d4b3da332f2793b53d62a49ed7b3f6349b9059404 +size 27770 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMoleculeWithResIcon_null_IconTitleSubtitleMoleculeWithResIcon-Day_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMoleculeWithResIcon_null_IconTitleSubtitleMoleculeWithResIcon-Day_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..461a325d97 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMoleculeWithResIcon_null_IconTitleSubtitleMoleculeWithResIcon-Day_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2050a2e304b492d54d2cc20ee65f02f7ae0d380a33e207b41e2bb8a8cae4dac8 +size 10117 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMoleculeWithResIcon_null_IconTitleSubtitleMoleculeWithResIcon-Night_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMoleculeWithResIcon_null_IconTitleSubtitleMoleculeWithResIcon-Night_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ae59789889 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_IconTitleSubtitleMoleculeWithResIcon_null_IconTitleSubtitleMoleculeWithResIcon-Night_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfb15c08455c3b7c323ed81362d56beddf84a387ff06d30336dbfab75b4df8a9 +size 9732 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Day_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Day_0_null,NEXUS_5,1.0,en].png index f87e41e29f..967a869007 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Day_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Day_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d3e7c8e6fa22c73e34ef27c7b6dc2c5780f48ad9b0680e5a0310896c0f11512 -size 19510 +oid sha256:4621864865a1acb0a4d8d502f1a75a3a888b6e6db92fa74fc24c4d93fd72cc28 +size 19488 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Night_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Night_1_null,NEXUS_5,1.0,en].png index ebf76be6fc..3678bc1d31 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Night_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_InfoListItemMolecule_null_InfoListItemMolecule-Night_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:240f32ad0c193c37d1533dd12500cad19ea52b5f7c36555dc734b371488eb750 -size 18820 +oid sha256:f1f088b90404dd0905e6799ea29912387c50262856ea25ad2f265c3ae0f0eacc +size 18833 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Day_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Day_0_null,NEXUS_5,1.0,en].png index b98fc16eeb..a2016c4e6d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Day_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Day_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:897ee73ef00a3a322a5ab99b6b3d39945e0b62fea72f1099ace542e4cff893cf -size 13104 +oid sha256:6f4fcfd8a51f291510c25abaec2437ca46bd7bd70e83a4f77fb304210854181e +size 13141 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Night_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Night_1_null,NEXUS_5,1.0,en].png index d703c086a7..3b617db3ce 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Night_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.organisms_InfoListOrganism_null_InfoListOrganism-Night_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:770f21753ce0d8f85e8e20b7d74f4ff903da693e5ca9704ef6f2430895656454 -size 12437 +oid sha256:bfe2fb59100daf7b057e6c6b128b3684d4956331f7faaa2324e10d435c9f8234 +size 12463 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ec81aa14bc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_60,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7c46e25d0b339f237f5c5d98d850664638b2084b3fe4a75e5d7b50d3b678b8b +size 18999 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..27df947568 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_61,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:150055f181351f24e22ba29492904b2296edc7c826aaa81952216734ec4431f9 +size 18098 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0f2ea2dda3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_Avatar_null_Avatars_Avatar_0_null_62,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a623b5b5bd5700f38b9eb790a37a5b029e1a74b8ae9811f9732dec808cacc770 +size 21231 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_OnboardingBackground_null_OnboardingBackground-Day_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_OnboardingBackground_null_OnboardingBackground-Day_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..89932eb28e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_OnboardingBackground_null_OnboardingBackground-Day_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4df7164831cfda683ae0f312130f5e1222439000df456975e6c16567dbd4888d +size 56615 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_OnboardingBackground_null_OnboardingBackground-Night_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_OnboardingBackground_null_OnboardingBackground-Night_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..17cb751284 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components_OnboardingBackground_null_OnboardingBackground-Night_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:070dd4ea130ee33d04b1d2e83facbc36be3126b21abfff5c78446fe6120f175b +size 51628 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_3,NEXUS_5,1.0,en].png index cb034df363..0a79fb8359 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9da1acd875be13ce9c7bed7de1b1eebf9858ea9ce076ba5a0319922d9f0e4f2f -size 74409 +oid sha256:cb7bef38ce2fa1c442811d78d6cf86a0e7b363f7896fc719a40b8c8e9d6a24dc +size 75609 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_4,NEXUS_5,1.0,en].png index b8078360dd..554f1677aa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Day_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c743a9b3c170fc6290584d6823156c0a56fc319b78a459784d2c9a3e1a583413 -size 58288 +oid sha256:42c6bee9a251f7052a9243c95f21d25b2e240983a1d3760425404762bb4408e1 +size 60068 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_3,NEXUS_5,1.0,en].png index 849de43d06..c988e24e01 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d208b98f67c6ff538624a72c782f50362bd2eb2d789237cee4b833bcbff694f -size 70801 +oid sha256:b04f471a108f8b7c5134584618d600e6a8613cbeb7e0dbe09341133268bec8e3 +size 71888 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_4,NEXUS_5,1.0,en].png index 3c578fbc70..9245997646 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.icons_IconsCompound_null_IconsCompound-Night_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b592fd9d733c2bbf7b59cbddfecab371b459f855d9421acaabd3ed97d39a2c3 -size 55536 +oid sha256:87a3cdfa5c3c1e6f01baf60ffeebddb76c1ae040bbed15fb9e79a0227c2c20a1 +size 56518 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierInsideSquare_null_SquareSizeModifierInsideSquare_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierInsideSquare_null_SquareSizeModifierInsideSquare_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3659321678 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierInsideSquare_null_SquareSizeModifierInsideSquare_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2aa126c2a29cba21d2304b551af552c0fa9de8e688375bd38721a910817c2ad4 +size 5409 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierLargeHeight_null_SquareSizeModifierLargeHeight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierLargeHeight_null_SquareSizeModifierLargeHeight_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5b5e11df0c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierLargeHeight_null_SquareSizeModifierLargeHeight_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a9255aeb0888547ea5d69f2cda3af3eba38b7107c2b76f950bd0756a4943c2c +size 5499 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierLargeWidth_null_SquareSizeModifierLargeWidth_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierLargeWidth_null_SquareSizeModifierLargeWidth_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2e98190424 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.modifiers_SquareSizeModifierLargeWidth_null_SquareSizeModifierLargeWidth_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b268b1a973f0743bcc328bcbe843b01061209b2d7bc80fe735090820d9a9705 +size 4855 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b232885ed5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7a8e9790e7d4b92e54a4161340c77f691685c28e9d40f272fb2c98808fec154 +size 32662 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..17ea7ac2f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5abc8664b6ef86b8f3357207635a3c038d78e3ff907ca56526242186c196cc22 +size 21152 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4b53087f1c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:504c858c17ef7229ba2bd52f3be7a6769ea2031a97fc7a39461a523cba120e9e +size 21512 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9ee728e547 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07ad31cb2c341ecddf9963ff93bdd9818f6209c9331c89cac9122293f76097cd +size 32472 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..26cbd8a27f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05a287eaae96eb6c1d2aabf624d10a1ddf97f50cd9c680a65b0496123fe53b46 +size 26294 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..811a6f1f65 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2a11615004b3b883be7093086a42efda0efabcb03cd580ec813fc827be02937 +size 38878 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..03e98787ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:853b90c82ad1dbbd9c1fa75feaa2115445fbbd6f674fa41f486826bd1ef89b1c +size 27022 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a0f85489dd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Day-0_1_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb5f63b160d04544705cf778774d046dfc2915f49fe4adfaa473111c95c86be8 +size 25731 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2eb2fc50d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01d2e5fdc0a9f1c5d534c965b0b6f30162dcb14d06e89af9eacc4c0084a13cdc +size 29052 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..99e171d097 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99bb674c5cfdbf86789426fdfb676f84d244b0452ba557915d803d78e4959224 +size 20188 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8c6c088e36 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac08cfe788fa4bd19f405862050a49da399c51b6921c977937a022a3f180f7b +size 20559 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f3877ac388 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a31489b3764a8eafe386fbcaf0b806d2acfeb7c92816230cba77e1f84704d645 +size 29818 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0e6149ea48 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af94bf79e1602817e800adb9a382681a9bf63b4435b9d5c7437a9a6a22d8c373 +size 24581 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..73e0749733 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ca71cd36ee1679c55eac28fc271f02a5dbbfc33d08202137d93a93e7285ac45 +size 35533 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..840c0da5d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d7937d6b674014e42d2b7c95104d51da0644f3557d91ea04e4ec9b626a2f4a0 +size 25044 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b878f43791 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.troubleshoot.impl_TroubleshootNotificationsView_null_TroubleshootNotificationsView-Night-0_2_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75cec48fca9ca9d551b18fbfabd318c72c33eec16743984ae2be3409f22a8d47 +size 23902 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b3ad636c86..ac9c773e1a 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -48,7 +48,8 @@ { "name" : ":features:verifysession:impl", "includeRegex" : [ - "screen_session_verification_.*" + "screen_session_verification_.*", + "screen_identity_.*" ] }, { @@ -80,7 +81,29 @@ "name" : ":libraries:push:impl", "includeRegex" : [ "push_.*", - "notification_.*" + "notification_.*", + "troubleshoot_notifications_test_current_push_provider.*", + "troubleshoot_notifications_test_detect_push_provider.*", + "troubleshoot_notifications_test_display_notification_.*", + "troubleshoot_notifications_test_push_loop_back_.*" + ] + }, + { + "name" : ":libraries:permissions:impl", + "includeRegex" : [ + "troubleshoot_notifications_test_check_permission_.*" + ] + }, + { + "name" : ":libraries:pushproviders:firebase", + "includeRegex" : [ + "troubleshoot_notifications_test_firebase_.*" + ] + }, + { + "name" : ":libraries:pushproviders:unifiedpush", + "includeRegex" : [ + "troubleshoot_notifications_test_unified_push_.*" ] }, { @@ -156,7 +179,8 @@ "name" : ":features:ftue:impl", "includeRegex" : [ "screen_welcome_.*", - "screen_notification_optin_.*" + "screen_notification_optin_.*", + "screen_qr_code_login_.+" ] }, { @@ -172,7 +196,8 @@ "includeRegex" : [ "screen_chat_backup_.*", "screen_key_backup_disable_.*", - "screen_recovery_key_.*" + "screen_recovery_key_.*", + "screen_create_new_recovery_key_.*" ] }, { @@ -182,7 +207,14 @@ "screen\\.advanced_settings\\..*", "screen_edit_profile_.*", "screen_notification_settings_.*", - "screen_blocked_users_.*" + "screen_blocked_users_.*", + "troubleshoot_notifications_entry_point_.*" + ] + }, + { + "name" : ":libraries:troubleshoot:impl", + "includeRegex" : [ + "troubleshoot_notifications_screen_.*" ] }, { @@ -197,6 +229,12 @@ "screen_app_lock_.*", "screen_signout_in_progress_dialog_content" ] + }, + { + "name" : ":features:roomdirectory:impl", + "includeRegex" : [ + "screen_room_directory_.*" + ] } ] }