From 752da3738308196a9d9c1c9013a7addcc5d19d48 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 22 Sep 2023 16:16:52 +0100 Subject: [PATCH 001/281] Add roomsWithUserDefinedRules data and render list - get roomsWithUserDefinedRules from rust - add to state in the presenter - render in the edit defaults view as a list --- ...EditDefaultNotificationSettingPresenter.kt | 34 ++++++++++++ .../EditDefaultNotificationSettingState.kt | 2 + .../EditDefaultNotificationSettingView.kt | 54 +++++++++++++++++++ ...efaultNotificationSettingsStateProvider.kt | 49 +++++++++++++++++ .../components/avatar/AvatarSize.kt | 2 + .../NotificationSettingsService.kt | 1 + .../RustNotificationSettingsService.kt | 5 ++ 7 files changed, 147 insertions(+) create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt index 764b37c52d..58140cb35f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -26,19 +26,25 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.roomlist.RoomSummary import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import java.text.Collator import kotlin.time.Duration.Companion.seconds class EditDefaultNotificationSettingPresenter @AssistedInject constructor( private val notificationSettingsService: NotificationSettingsService, @Assisted private val isOneToOne: Boolean, + private val roomListService: RoomListService, + private val matrixClient: MatrixClient, ) : Presenter { @AssistedFactory interface Factory { @@ -50,10 +56,16 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( val mode: MutableState = remember { mutableStateOf(null) } + + val roomsWithUserDefinedMode: MutableState> = remember { + mutableStateOf(listOf()) + } + val localCoroutineScope = rememberCoroutineScope() LaunchedEffect(Unit) { fetchSettings(mode) observeNotificationSettings(mode) + observeRoomSummaries(roomsWithUserDefinedMode) } fun handleEvents(event: EditDefaultNotificationSettingStateEvents) { @@ -65,6 +77,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( return EditDefaultNotificationSettingState( isOneToOne = isOneToOne, mode = mode.value, + roomsWithUserDefinedMode = roomsWithUserDefinedMode.value, eventSink = ::handleEvents ) } @@ -83,6 +96,27 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( .launchIn(this) } + private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState>) { + roomListService.allRooms() + .summaries + .onEach { + updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode) + } + .launchIn(this) + } + + private fun CoroutineScope.updateRoomsWithUserDefinedMode(summaries: List, roomsWithUserDefinedMode: MutableState>) = launch { + val roomWithUserDefinedRules = notificationSettingsService.getRoomsWithUserDefinedRules().getOrThrow().toSet() + roomsWithUserDefinedMode.value = summaries + .filterIsInstance() + .filter { + val room = matrixClient.getRoom(it.details.roomId) ?: return@filter false + roomWithUserDefinedRules.contains(it.identifier()) && isOneToOne == room.isOneToOne + } + // locale sensitive sorting + .sortedWith(compareBy(Collator.getInstance()){ it.details.name }) + } + private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode) = launch { // On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did). notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt index 62c708d988..e4d18239cd 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt @@ -17,9 +17,11 @@ package io.element.android.features.preferences.impl.notifications.edit import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.roomlist.RoomSummary data class EditDefaultNotificationSettingState( val isOneToOne: Boolean, val mode: RoomNotificationMode?, + val roomsWithUserDefinedMode: List, val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt index 4cc95af71f..56cf3acf9a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt @@ -17,12 +17,26 @@ package io.element.android.features.preferences.impl.notifications.edit import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectableGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.preferences.impl.notifications.NotificationSettingsState +import io.element.android.features.preferences.impl.notifications.NotificationSettingsStateProvider +import io.element.android.features.preferences.impl.notifications.NotificationSettingsView +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.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceView +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.ui.strings.CommonStrings @@ -70,6 +84,46 @@ fun EditDefaultNotificationSettingView( } } } + if(state.roomsWithUserDefinedMode.isNotEmpty()) { + PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_edit_custom_settings_section_title)) { + LazyColumn { + items(state.roomsWithUserDefinedMode) { summary -> + val subtitle = when (summary.details.notificationMode) { + RoomNotificationMode.ALL_MESSAGES -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_all_messages) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_mentions_and_keywords) + RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute) + null -> "" + } + val avatarData = AvatarData( + id = summary.identifier(), + name = summary.details.name, + url = summary.details.avatarURLString, + size = AvatarSize.CustomRoomNotificationSetting, + ) + ListItem( + headlineContent = { + Text(text = summary.details.name) + }, + supportingContent = { + Text(text = subtitle) + }, + leadingContent = ListItemContent.Custom { + Avatar(avatarData = avatarData) + } + ) + } + } + } + } + } } +@DayNightPreviews +@Composable +internal fun EditDefaultNotificationSettingViewPreview(@PreviewParameter(EditDefaultNotificationSettingsStateProvider::class) state: EditDefaultNotificationSettingState) = ElementPreview { + EditDefaultNotificationSettingView( + state = state, + onBackPressed = {}, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt new file mode 100644 index 0000000000..738074e9e6 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt @@ -0,0 +1,49 @@ +/* + * 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.preferences.impl.notifications.edit + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails + +open class EditDefaultNotificationSettingsStateProvider: PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anEditDefaultNotificationSettingsState(), + ) +} + +fun anEditDefaultNotificationSettingsState() = EditDefaultNotificationSettingState( + isOneToOne = false, + mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, + roomsWithUserDefinedMode = listOf(aRoomSummary()), + eventSink = {} +) + +private fun aRoomSummary() = RoomSummary.Filled( + RoomSummaryDetails( + roomId = RoomId("!roomId:domain"), + name = "Room", + avatarURLString = null, + isDirect = false, + lastMessage = null, + lastMessageTimestamp = null, + unreadNotificationCount = 0, + ) +) 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 45d7780393..b2004ed204 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 @@ -46,4 +46,6 @@ enum class AvatarSize(val dp: Dp) { EditRoomDetails(70.dp), NotificationsOptIn(32.dp), + + CustomRoomNotificationSetting(36.dp) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt index 5a81edb052..71d46a2b8e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt @@ -38,4 +38,5 @@ interface NotificationSettingsService { suspend fun setRoomMentionEnabled(enabled: Boolean): Result suspend fun isCallEnabled(): Result suspend fun setCallEnabled(enabled: Boolean): Result + suspend fun getRoomsWithUserDefinedRules(): Result> } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt index a2fffdbdfb..df5c4d577f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -110,4 +110,9 @@ class RustNotificationSettingsService( notificationSettings.setCallEnabled(enabled) } } + + override suspend fun getRoomsWithUserDefinedRules(): Result> = + runCatching { + notificationSettings.getRoomsWithUserDefinedRules(enabled = true) + } } From 9ded4284b2b8a4b1124a27a7eb73d24366485bec Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Oct 2023 18:09:46 +0200 Subject: [PATCH 002/281] Setup the pin feature module --- features/pin/api/build.gradle.kts | 27 +++++++ .../android/features/pin/api/PinEntryPoint.kt | 37 +++++++++ features/pin/impl/build.gradle.kts | 52 +++++++++++++ .../features/pin/impl/DefaultPinEntryPoint.kt | 46 +++++++++++ .../android/features/pin/impl/PinFlowNode.kt | 78 +++++++++++++++++++ .../pin/impl/auth/PinAuthenticationEvents.kt | 21 +++++ .../pin/impl/auth/PinAuthenticationNode.kt | 44 +++++++++++ .../impl/auth/PinAuthenticationPresenter.kt | 38 +++++++++ .../pin/impl/auth/PinAuthenticationState.kt | 21 +++++ .../auth/PinAuthenticationStateProvider.kt | 31 ++++++++ .../pin/impl/auth/PinAuthenticationView.kt | 50 ++++++++++++ .../pin/impl/create/CreatePinEvents.kt | 21 +++++ .../features/pin/impl/create/CreatePinNode.kt | 44 +++++++++++ .../pin/impl/create/CreatePinPresenter.kt | 38 +++++++++ .../pin/impl/create/CreatePinState.kt | 21 +++++ .../pin/impl/create/CreatePinStateProvider.kt | 31 ++++++++ .../features/pin/impl/create/CreatePinView.kt | 49 ++++++++++++ 17 files changed, 649 insertions(+) create mode 100644 features/pin/api/build.gradle.kts create mode 100644 features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt create mode 100644 features/pin/impl/build.gradle.kts create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt diff --git a/features/pin/api/build.gradle.kts b/features/pin/api/build.gradle.kts new file mode 100644 index 0000000000..95b062b0c8 --- /dev/null +++ b/features/pin/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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.features.pin.api" +} + +dependencies { + implementation(projects.libraries.architecture) +} diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt new file mode 100644 index 0000000000..df0bf255f4 --- /dev/null +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt @@ -0,0 +1,37 @@ +/* + * 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.pin.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 PinEntryPoint : FeatureEntryPoint { + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + // Add your callbacks + } +} + diff --git a/features/pin/impl/build.gradle.kts b/features/pin/impl/build.gradle.kts new file mode 100644 index 0000000000..6bf14646e3 --- /dev/null +++ b/features/pin/impl/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.pin.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.pin.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + + ksp(libs.showkase.processor) +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt new file mode 100644 index 0000000000..1dfba1a4cf --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt @@ -0,0 +1,46 @@ +/* + * 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.pin.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.pin.api.PinEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultPinEntryPoint @Inject constructor() : PinEntryPoint { + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PinEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : PinEntryPoint.NodeBuilder { + + override fun callback(callback: PinEntryPoint.Callback): PinEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt new file mode 100644 index 0000000000..a76504ce8a --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt @@ -0,0 +1,78 @@ +/* + * 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.pin.impl + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.pin.impl.auth.PinAuthenticationNode +import io.element.android.features.pin.impl.create.CreatePinNode +import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import kotlinx.parcelize.Parcelize + +@ContributesNode(AppScope::class) +class PinFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Auth, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Auth : NavTarget + + @Parcelize + data object Create : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Auth -> { + createNode(buildContext) + } + NavTarget.Create -> { + createNode(buildContext) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler(), + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt new file mode 100644 index 0000000000..a56412aa6e --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt @@ -0,0 +1,21 @@ +/* + * 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.pin.impl.auth + +sealed interface PinAuthenticationEvents { + object MyEvent : PinAuthenticationEvents +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt new file mode 100644 index 0000000000..b5dab44c96 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt @@ -0,0 +1,44 @@ +/* + * 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.pin.impl.auth + +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.AppScope + +@ContributesNode(AppScope::class) +class PinAuthenticationNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: PinAuthenticationPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + PinAuthenticationView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt new file mode 100644 index 0000000000..79eace0072 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt @@ -0,0 +1,38 @@ +/* + * 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.pin.impl.auth + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class PinAuthenticationPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): PinAuthenticationState { + + fun handleEvents(event: PinAuthenticationEvents) { + when (event) { + PinAuthenticationEvents.MyEvent -> Unit + } + } + + return PinAuthenticationState( + eventSink = ::handleEvents + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt new file mode 100644 index 0000000000..2df1e50f83 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt @@ -0,0 +1,21 @@ +/* + * 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.pin.impl.auth + +data class PinAuthenticationState( + val eventSink: (PinAuthenticationEvents) -> Unit +) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt new file mode 100644 index 0000000000..7aea002ee9 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.pin.impl.auth + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class PinAuthenticationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPinAuthenticationState(), + // Add other states here + ) +} + +fun aPinAuthenticationState() = PinAuthenticationState( + eventSink = {} +) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt new file mode 100644 index 0000000000..b318eebe79 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt @@ -0,0 +1,50 @@ +/* + * 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.pin.impl.auth + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +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 + +@Composable +fun PinAuthenticationView( + state: PinAuthenticationState, + modifier: Modifier = Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + Text( + "PinAuthentication feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@Composable +@PreviewsDayNight +fun PinAuthenticationViewLightPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) = + ElementPreview { + PinAuthenticationView( + state = state, + ) + } + diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt new file mode 100644 index 0000000000..280856b5c8 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt @@ -0,0 +1,21 @@ +/* + * 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.pin.impl.create + +sealed interface CreatePinEvents { + object MyEvent : CreatePinEvents +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt new file mode 100644 index 0000000000..0ed0343a5b --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt @@ -0,0 +1,44 @@ +/* + * 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.pin.impl.create + +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.AppScope + +@ContributesNode(AppScope::class) +class CreatePinNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: CreatePinPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + CreatePinView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt new file mode 100644 index 0000000000..d45257b4bd --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt @@ -0,0 +1,38 @@ +/* + * 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.pin.impl.create + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class CreatePinPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): CreatePinState { + + fun handleEvents(event: CreatePinEvents) { + when (event) { + CreatePinEvents.MyEvent -> Unit + } + } + + return CreatePinState( + eventSink = ::handleEvents + ) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt new file mode 100644 index 0000000000..c405db82ec --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt @@ -0,0 +1,21 @@ +/* + * 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.pin.impl.create + +data class CreatePinState( + val eventSink: (CreatePinEvents) -> Unit +) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt new file mode 100644 index 0000000000..4bff72023e --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.pin.impl.create + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class CreatePinStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aCreatePinState(), + // Add other states here + ) +} + +fun aCreatePinState() = CreatePinState( + eventSink = {} +) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt new file mode 100644 index 0000000000..871d440a89 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt @@ -0,0 +1,49 @@ +/* + * 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.pin.impl.create + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +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 + +@Composable +fun CreatePinView( + state: CreatePinState, + modifier: Modifier = Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + Text( + "CreatePin feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@Composable +@PreviewsDayNight +fun CreatePinViewLightPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) = + ElementPreview { + CreatePinView( + state = state, + ) + } From 2d5a3a473cb00edad6bd0e5f8028638ff81a8b47 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Oct 2023 16:25:27 +0200 Subject: [PATCH 003/281] Pin setup with fake lock --- .../io/element/android/appnav/BackstackExt.kt | 21 ++++++++ .../android/appnav/LoggedInFlowNode.kt | 47 ++++++++++++----- .../io/element/android/appnav/RootFlowNode.kt | 1 + .../android/appnav/loggedin/LoggedInNode.kt | 5 +- .../android/features/pin/api/PinState.kt | 22 ++++++++ .../features/pin/api/PinStateDataSource.kt | 26 ++++++++++ .../pin/impl/auth/PinAuthenticationEvents.kt | 2 +- .../impl/auth/PinAuthenticationPresenter.kt | 9 ++-- .../auth/PinAuthenticationStateProvider.kt | 1 - .../pin/impl/auth/PinAuthenticationView.kt | 51 ++++++++++++++++--- .../impl/state/DefaultPinStateDataSource.kt | 42 +++++++++++++++ .../molecules/IconTitleSubtitleMolecule.kt | 24 +++++---- 12 files changed, 211 insertions(+), 40 deletions(-) create mode 100644 features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt create mode 100644 features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt index 36b267debb..73bb9b9b85 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt @@ -16,6 +16,12 @@ package io.element.android.appnav +import android.content.Context +import android.content.ContextWrapper +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.NewRoot import com.bumble.appyx.navmodel.backstack.operation.Remove @@ -41,3 +47,18 @@ fun BackStack.removeLast(element: T) { accept(Remove(lastExpectedNavElement.key)) } +@Composable +fun FinishActivityBackHandler(enabled: Boolean = true) { + + fun Context.findActivity(): ComponentActivity? = when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null + } + + val context = LocalContext.current + BackHandler(enabled = enabled) { + context.findActivity()?.finish() + } +} + 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 cf67a765d7..276f0ebbde 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -50,6 +50,9 @@ import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.invitelist.api.InviteListEntryPoint import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.features.pin.api.PinEntryPoint +import io.element.android.features.pin.api.PinState +import io.element.android.features.pin.api.PinStateDataSource import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -90,6 +93,8 @@ class LoggedInFlowNode @AssistedInject constructor( private val networkMonitor: NetworkMonitor, private val notificationDrawerManager: NotificationDrawerManager, private val ftueState: FtueState, + private val pinEntryPoint: PinEntryPoint, + private val pinStateDataSource: PinStateDataSource, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( @@ -98,7 +103,7 @@ class LoggedInFlowNode @AssistedInject constructor( savedStateMap = buildContext.savedStateMap, ), permanentNavModel = PermanentNavModel( - NavTarget.Permanent, + navTargets = setOf(NavTarget.LoggedInPermanent, NavTarget.LockPermanent), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -130,6 +135,7 @@ class LoggedInFlowNode @AssistedInject constructor( } }, onStop = { + pinStateDataSource.lock() //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. coroutineScope.launch { syncService.stopSync() @@ -167,7 +173,10 @@ class LoggedInFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - data object Permanent : NavTarget + data object LoggedInPermanent : NavTarget + + @Parcelize + data object LockPermanent : NavTarget @Parcelize data object RoomList : NavTarget @@ -196,9 +205,12 @@ class LoggedInFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Permanent -> { + NavTarget.LoggedInPermanent -> { createNode(buildContext) } + NavTarget.LockPermanent -> { + pinEntryPoint.nodeBuilder(this, buildContext).build() + } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { override fun onRoomClicked(roomId: RoomId) { @@ -324,17 +336,24 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { - Children( - navModel = backstack, - modifier = Modifier, - // Animate navigation to settings and to a room - transitionHandler = rememberDefaultTransitionHandler(), - ) - - val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState() - - if (!isFtueDisplayed) { - PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.Permanent) + val pinState by pinStateDataSource.pinState.collectAsState() + when (pinState) { + PinState.Unlocked -> { + Children( + navModel = backstack, + modifier = Modifier, + // Animate navigation to settings and to a room + transitionHandler = rememberDefaultTransitionHandler(), + ) + val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState() + if (!isFtueDisplayed) { + PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) + } + } + PinState.Locked -> { + FinishActivityBackHandler() + PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent) + } } } } 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 94f344be7e..403b53f0e1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -44,6 +44,7 @@ import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcActionFlow +import io.element.android.features.pin.api.PinEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt index 6950b9b699..5ddbb164d8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInNode.kt @@ -31,7 +31,10 @@ class LoggedInNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val loggedInPresenter: LoggedInPresenter, -) : Node(buildContext, plugins = plugins) { +) : Node( + buildContext = buildContext, + plugins = plugins +) { @Composable override fun View(modifier: Modifier) { diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt new file mode 100644 index 0000000000..0ff1b0b3d5 --- /dev/null +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt @@ -0,0 +1,22 @@ +/* + * 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.pin.api + +sealed interface PinState { + data object Unlocked : PinState + data object Locked : PinState +} diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt new file mode 100644 index 0000000000..c0b52af8b7 --- /dev/null +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt @@ -0,0 +1,26 @@ +/* + * 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.pin.api + +import kotlinx.coroutines.flow.StateFlow + +interface PinStateDataSource { + val pinState: StateFlow + + fun lock() + fun unlock() +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt index a56412aa6e..110c62660a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt @@ -17,5 +17,5 @@ package io.element.android.features.pin.impl.auth sealed interface PinAuthenticationEvents { - object MyEvent : PinAuthenticationEvents + data object Unlock : PinAuthenticationEvents } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt index 79eace0072..36970f34ab 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt @@ -17,20 +17,21 @@ package io.element.android.features.pin.impl.auth import androidx.compose.runtime.Composable +import io.element.android.features.pin.api.PinStateDataSource import io.element.android.libraries.architecture.Presenter import javax.inject.Inject -class PinAuthenticationPresenter @Inject constructor() : Presenter { +class PinAuthenticationPresenter @Inject constructor( + private val pinStateDataSource: PinStateDataSource, +) : Presenter { @Composable override fun present(): PinAuthenticationState { - fun handleEvents(event: PinAuthenticationEvents) { when (event) { - PinAuthenticationEvents.MyEvent -> Unit + PinAuthenticationEvents.Unlock -> pinStateDataSource.unlock() } } - return PinAuthenticationState( eventSink = ::handleEvents ) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt index 7aea002ee9..8e3f45ac07 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt @@ -22,7 +22,6 @@ open class PinAuthenticationStateProvider : PreviewParameterProvider get() = sequenceOf( aPinAuthenticationState(), - // Add other states here ) } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt index b318eebe79..9aa2527099 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt @@ -16,29 +16,64 @@ package io.element.android.features.pin.impl.auth -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage 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 +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent @Composable fun PinAuthenticationView( state: PinAuthenticationState, modifier: Modifier = Modifier, ) { - Box(modifier, contentAlignment = Alignment.Center) { - Text( - "PinAuthentication feature view", - color = MaterialTheme.colorScheme.primary, + Surface(modifier) { + HeaderFooterPage( + modifier = Modifier + .systemBarsPadding() + .fillMaxSize(), + header = { PinAuthenticationHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, + footer = { PinAuthenticationFooter(state) }, ) } } +@Composable +private fun PinAuthenticationHeader( + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier, + title = "Element X is locked", + subTitle = null, + iconImageVector = Icons.Default.Lock, + ) +} + +@Composable +private fun PinAuthenticationFooter(state: PinAuthenticationState) { + Button( + modifier = Modifier.fillMaxWidth(), + text = "Unlock", + onClick = { + state.eventSink(PinAuthenticationEvents.Unlock) + } + ) +} + @Composable @PreviewsDayNight fun PinAuthenticationViewLightPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) = diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt new file mode 100644 index 0000000000..e0f8c6e6f9 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.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.pin.impl.state + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.pin.api.PinState +import io.element.android.features.pin.api.PinStateDataSource +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultPinStateDataSource @Inject constructor() : PinStateDataSource { + + private val _pinState = MutableStateFlow(PinState.Locked) + override val pinState: StateFlow = _pinState + + override fun unlock() { + _pinState.value = PinState.Unlocked + } + + override fun lock() { + _pinState.value = PinState.Locked + } +} 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 88bc258348..52d8eb5baf 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 @@ -31,8 +31,8 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize -import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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 import io.element.android.libraries.theme.ElementTheme @@ -49,7 +49,7 @@ import io.element.android.libraries.theme.ElementTheme @Composable fun IconTitleSubtitleMolecule( title: String, - subTitle: String, + subTitle: String?, modifier: Modifier = Modifier, iconResourceId: Int? = null, iconImageVector: ImageVector? = null, @@ -73,14 +73,16 @@ fun IconTitleSubtitleMolecule( style = ElementTheme.typography.fontHeadingMdBold, color = MaterialTheme.colorScheme.primary, ) - Spacer(Modifier.height(8.dp)) - Text( - text = subTitle, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, - ) + if (subTitle != null) { + Spacer(Modifier.height(8.dp)) + Text( + text = subTitle, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.secondary, + ) + } } } @@ -90,6 +92,6 @@ internal fun IconTitleSubtitleMoleculePreview() = ElementPreview { IconTitleSubtitleMolecule( iconResourceId = R.drawable.ic_compound_chat, title = "Title", - subTitle = "Sub iitle", + subTitle = "Subtitle", ) } From ea0963c0c8e6c4c658ea44cb160389f7da435080 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Oct 2023 20:25:51 +0200 Subject: [PATCH 004/281] Pin: use moveTaskToBack instead of finish --- .../src/main/kotlin/io/element/android/appnav/BackstackExt.kt | 4 ++-- .../main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt index 73bb9b9b85..e7bf9f200f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt @@ -48,7 +48,7 @@ fun BackStack.removeLast(element: T) { } @Composable -fun FinishActivityBackHandler(enabled: Boolean = true) { +fun MoveActivityToBackgroundBackHandler(enabled: Boolean = true) { fun Context.findActivity(): ComponentActivity? = when (this) { is ComponentActivity -> this @@ -58,7 +58,7 @@ fun FinishActivityBackHandler(enabled: Boolean = true) { val context = LocalContext.current BackHandler(enabled = enabled) { - context.findActivity()?.finish() + context.findActivity()?.moveTaskToBack(false) } } 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 276f0ebbde..6bc8c3afaf 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -351,7 +351,7 @@ class LoggedInFlowNode @AssistedInject constructor( } } PinState.Locked -> { - FinishActivityBackHandler() + MoveActivityToBackgroundBackHandler() PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent) } } From 304ec0b74045b832a1e974530a24f58d946fbaf7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Oct 2023 20:36:30 +0200 Subject: [PATCH 005/281] Pin unlock : hides behind feature flag (disabled by default) --- .../android/appnav/LoggedInFlowNode.kt | 4 ++-- .../features/pin/api/PinStateDataSource.kt | 4 ++-- features/pin/impl/build.gradle.kts | 1 + .../impl/auth/PinAuthenticationPresenter.kt | 6 +++++- .../impl/state/DefaultPinStateDataSource.kt | 20 +++++++++++++------ .../libraries/featureflag/api/FeatureFlags.kt | 6 ++++++ .../impl/StaticFeatureFlagProvider.kt | 1 + 7 files changed, 31 insertions(+), 11 deletions(-) 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 6bc8c3afaf..8879c39220 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -135,9 +135,9 @@ class LoggedInFlowNode @AssistedInject constructor( } }, onStop = { - pinStateDataSource.lock() - //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. coroutineScope.launch { + pinStateDataSource.lock() + //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. syncService.stopSync() } }, diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt index c0b52af8b7..5098f6c0a6 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt @@ -21,6 +21,6 @@ import kotlinx.coroutines.flow.StateFlow interface PinStateDataSource { val pinState: StateFlow - fun lock() - fun unlock() + suspend fun lock() + suspend fun unlock() } diff --git a/features/pin/impl/build.gradle.kts b/features/pin/impl/build.gradle.kts index 6bf14646e3..0d115ac46c 100644 --- a/features/pin/impl/build.gradle.kts +++ b/features/pin/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.featureflag.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt index 36970f34ab..a9660552ad 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt @@ -19,17 +19,21 @@ package io.element.android.features.pin.impl.auth import androidx.compose.runtime.Composable import io.element.android.features.pin.api.PinStateDataSource import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject class PinAuthenticationPresenter @Inject constructor( private val pinStateDataSource: PinStateDataSource, + private val coroutineScope: CoroutineScope, ) : Presenter { @Composable override fun present(): PinAuthenticationState { + fun handleEvents(event: PinAuthenticationEvents) { when (event) { - PinAuthenticationEvents.Unlock -> pinStateDataSource.unlock() + PinAuthenticationEvents.Unlock -> coroutineScope.launch { pinStateDataSource.unlock() } } } return PinAuthenticationState( diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt index e0f8c6e6f9..396d3a8d3a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt @@ -21,22 +21,30 @@ import io.element.android.features.pin.api.PinState import io.element.android.features.pin.api.PinStateDataSource import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultPinStateDataSource @Inject constructor() : PinStateDataSource { +class DefaultPinStateDataSource @Inject constructor( + private val featureFlagService: FeatureFlagService, +) : PinStateDataSource { - private val _pinState = MutableStateFlow(PinState.Locked) + private val _pinState = MutableStateFlow(PinState.Unlocked) override val pinState: StateFlow = _pinState - override fun unlock() { - _pinState.value = PinState.Unlocked + override suspend fun unlock() { + if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { + _pinState.value = PinState.Unlocked + } } - override fun lock() { - _pinState.value = PinState.Locked + override suspend fun lock() { + if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { + _pinState.value = PinState.Locked + } } } 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 8d35223986..121cf26271 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 @@ -49,4 +49,10 @@ enum class FeatureFlags( description = "Send and receive voice messages", defaultValue = false, ), + PinUnlock( + key = "feature.pinunlock", + title = "Pin unlock", + description = "Allow user to lock/unlock the app with a pin code or biometrics", + defaultValue = 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 7ef10262c9..48f159de83 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 @@ -36,6 +36,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> true FeatureFlags.VoiceMessages -> false + FeatureFlags.PinUnlock -> false } } else { false From bdcd2714cce5a98612ddea9ac12dfab4ca28bbeb Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Oct 2023 20:38:41 +0200 Subject: [PATCH 006/281] Pin : rename PinStateDataSource by PinStateService --- .../kotlin/io/element/android/appnav/LoggedInFlowNode.kt | 4 ++-- .../pin/api/{PinStateDataSource.kt => PinStateService.kt} | 2 +- .../features/pin/impl/auth/PinAuthenticationPresenter.kt | 4 ++-- ...faultPinStateDataSource.kt => DefaultPinStateService.kt} | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename features/pin/api/src/main/kotlin/io/element/android/features/pin/api/{PinStateDataSource.kt => PinStateService.kt} (96%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/{DefaultPinStateDataSource.kt => DefaultPinStateService.kt} (92%) 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 8879c39220..e09a17503a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -52,7 +52,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.pin.api.PinEntryPoint import io.element.android.features.pin.api.PinState -import io.element.android.features.pin.api.PinStateDataSource +import io.element.android.features.pin.api.PinStateService import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -94,7 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val ftueState: FtueState, private val pinEntryPoint: PinEntryPoint, - private val pinStateDataSource: PinStateDataSource, + private val pinStateService PinStateService, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt similarity index 96% rename from features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt rename to features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt index 5098f6c0a6..92ec8715ab 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateDataSource.kt +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt @@ -18,7 +18,7 @@ package io.element.android.features.pin.api import kotlinx.coroutines.flow.StateFlow -interface PinStateDataSource { +interface PinStateService { val pinState: StateFlow suspend fun lock() diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt index a9660552ad..754ca3a4fa 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt @@ -17,14 +17,14 @@ package io.element.android.features.pin.impl.auth import androidx.compose.runtime.Composable -import io.element.android.features.pin.api.PinStateDataSource +import io.element.android.features.pin.api.PinStateService import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class PinAuthenticationPresenter @Inject constructor( - private val pinStateDataSource: PinStateDataSource, + private val pinStateService PinStateService, private val coroutineScope: CoroutineScope, ) : Presenter { diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt similarity index 92% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt index 396d3a8d3a..e08a96f458 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateDataSource.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt @@ -18,7 +18,7 @@ package io.element.android.features.pin.impl.state import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.pin.api.PinState -import io.element.android.features.pin.api.PinStateDataSource +import io.element.android.features.pin.api.PinStateService import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -29,9 +29,9 @@ import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultPinStateDataSource @Inject constructor( +class DefaultPinStateService @Inject constructor( private val featureFlagService: FeatureFlagService, -) : PinStateDataSource { +) : PinStateService { private val _pinState = MutableStateFlow(PinState.Unlocked) override val pinState: StateFlow = _pinState From 588565995c1dd71ae9eeea0626e0bfe23a7d54a1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Oct 2023 20:52:19 +0200 Subject: [PATCH 007/281] Pin code : add simple grace period --- .../android/appnav/LoggedInFlowNode.kt | 15 ++++++++++--- .../features/pin/api/PinStateService.kt | 3 ++- .../impl/auth/PinAuthenticationPresenter.kt | 4 ++-- .../pin/impl/state/DefaultPinStateService.kt | 21 ++++++++++++++++--- 4 files changed, 34 insertions(+), 9 deletions(-) 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 e09a17503a..d1c975f5b6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -94,7 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val ftueState: FtueState, private val pinEntryPoint: PinEntryPoint, - private val pinStateService PinStateService, + private val pinStateService: PinStateService, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( @@ -134,9 +134,18 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.Ftue) } }, + onResume = { + coroutineScope.launch { + pinStateService.entersForeground() + } + }, + onPause = { + coroutineScope.launch { + pinStateService.entersBackground() + } + }, onStop = { coroutineScope.launch { - pinStateDataSource.lock() //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. syncService.stopSync() } @@ -336,7 +345,7 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { - val pinState by pinStateDataSource.pinState.collectAsState() + val pinState by pinStateService.pinState.collectAsState() when (pinState) { PinState.Unlocked -> { Children( diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt index 92ec8715ab..4ecb473c18 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.StateFlow interface PinStateService { val pinState: StateFlow - suspend fun lock() + suspend fun entersForeground() + suspend fun entersBackground() suspend fun unlock() } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt index 754ca3a4fa..5e7e274ba7 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject class PinAuthenticationPresenter @Inject constructor( - private val pinStateService PinStateService, + private val pinStateService: PinStateService, private val coroutineScope: CoroutineScope, ) : Presenter { @@ -33,7 +33,7 @@ class PinAuthenticationPresenter @Inject constructor( fun handleEvents(event: PinAuthenticationEvents) { when (event) { - PinAuthenticationEvents.Unlock -> coroutineScope.launch { pinStateDataSource.unlock() } + PinAuthenticationEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } } } return PinAuthenticationState( diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt index e08a96f458..69371c40ee 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt @@ -23,10 +23,16 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import javax.inject.Inject +private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L + @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultPinStateService @Inject constructor( @@ -36,15 +42,24 @@ class DefaultPinStateService @Inject constructor( private val _pinState = MutableStateFlow(PinState.Unlocked) override val pinState: StateFlow = _pinState + private var lockJob: Job? = null + override suspend fun unlock() { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { _pinState.value = PinState.Unlocked } } - override suspend fun lock() { - if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - _pinState.value = PinState.Locked + override suspend fun entersForeground() { + lockJob?.cancel() + } + + override suspend fun entersBackground(): Unit = coroutineScope { + lockJob = launch { + if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { + delay(GRACE_PERIOD_IN_MILLIS) + _pinState.value = PinState.Locked + } } } } From 77cacb1ee666e04a3dc4010d2dfb36fb605de404 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 11:06:56 +0200 Subject: [PATCH 008/281] version++ --- plugins/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 45e5b4adfd..94f40c788a 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -56,7 +56,7 @@ private const val versionMinor = 2 // 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 = 4 +private const val versionPatch = 5 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch From 4a7b40fe175eaabbea8cc020de842a79c06f5fc8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 12:05:22 +0200 Subject: [PATCH 009/281] Remove unused import --- .../android/features/roomlist/impl/RoomListStateProvider.kt | 1 - 1 file changed, 1 deletion(-) 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 2a27777e4a..7199bfd227 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 @@ -17,7 +17,6 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders From a3e6d691a5a89a701fbe48239f3c1c2ff5788694 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 12 Oct 2023 12:29:53 +0200 Subject: [PATCH 010/281] Pin setup : clean up --- .../main/kotlin/io/element/android/appnav/RootFlowNode.kt | 1 - .../android/features/pin/impl/auth/PinAuthenticationView.kt | 5 ++--- .../android/features/pin/impl/create/CreatePinView.kt | 5 ++++- .../features/pin/impl/state/DefaultPinStateService.kt | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 403b53f0e1..94f344be7e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -44,7 +44,6 @@ import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcActionFlow -import io.element.android.features.pin.api.PinEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt index 9aa2527099..9fe689bb39 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt @@ -26,14 +26,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage 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.Surface -import io.element.android.libraries.designsystem.utils.OnLifecycleEvent @Composable fun PinAuthenticationView( @@ -76,10 +74,11 @@ private fun PinAuthenticationFooter(state: PinAuthenticationState) { @Composable @PreviewsDayNight -fun PinAuthenticationViewLightPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) = +internal fun PinAuthenticationViewPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) { ElementPreview { PinAuthenticationView( state = state, ) } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt index 871d440a89..efdbe62bfa 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt @@ -25,12 +25,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter 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 +import timber.log.Timber @Composable fun CreatePinView( state: CreatePinState, modifier: Modifier = Modifier, ) { + Timber.d("CreatePinView: $state") Box(modifier, contentAlignment = Alignment.Center) { Text( "CreatePin feature view", @@ -41,9 +43,10 @@ fun CreatePinView( @Composable @PreviewsDayNight -fun CreatePinViewLightPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) = +internal fun CreatePinViewLightPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { ElementPreview { CreatePinView( state = state, ) } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt index 69371c40ee..9accef2b80 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt @@ -54,7 +54,7 @@ class DefaultPinStateService @Inject constructor( lockJob?.cancel() } - override suspend fun entersBackground(): Unit = coroutineScope { + override suspend fun entersBackground() = coroutineScope { lockJob = launch { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { delay(GRACE_PERIOD_IN_MILLIS) From c5742b146b5bb691760cc82e888c60728b824dca Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 12 Oct 2023 10:45:08 +0000 Subject: [PATCH 011/281] Update screenshots --- ...ull_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ull_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...e_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...e_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ull_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ull_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png | 4 ++-- 6 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f1992899f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f19d53c688e3f862894775756f00040adb5cbba99de71c053ed503c1b8af9518 +size 15239 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7a21c8025 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11c145595f7713bc7b66f9d07e917bca82d6e06a1b55367801eb4d2cbfef89b0 +size 14353 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e8dd1a28d7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9914a33ba23544bdfce1e21b52ad247024392730fb22b60bc9b6fa6440f004d4 +size 9216 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..157a7c52c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ad524a918e499fcea6fd5293358167ff52f8877cc31b778c8def01925fa662f +size 8582 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png index 9b578f2a48..8fb30525d0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5a887e4fa9170810c5567933b9c1a80c9ffbbb7b5b9f7187de9d0331482a78a -size 10619 +oid sha256:b514737761239d60aac83a2b5d822c1079b7029474097f5ea4a4a29a88174e81 +size 10687 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png index 1d604d08ee..95a1fb946e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_IconTitleSubtitleMolecule-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d65a93f6e93d3725f51bb48b1d7dabaac4c4b10cb0897024a02966c04bfc3d5 -size 10508 +oid sha256:d648d16fa94fa4add5784cb0ca32173cc447dfd592bcf88ff82b53894c02f137 +size 10527 From a2601c0388172cdca83b766532538f89e68790d6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 14:15:22 +0200 Subject: [PATCH 012/281] Remove unused import --- .../features/roomdetails/impl/RoomDetailsStateProvider.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index bbbcc64da6..72ef265749 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -17,7 +17,6 @@ package io.element.android.features.roomdetails.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState import io.element.android.libraries.matrix.api.core.UserId From 193852520116411b8621166ff261714e783150c9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 14:51:36 +0200 Subject: [PATCH 013/281] Fix DB migration test. Does not require a migration apparently. --- .../impl/src/main/sqldelight/databases/4.db | Bin 12288 -> 12288 bytes .../libraries/matrix/session/SessionData.sq | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/4.db b/libraries/session-storage/impl/src/main/sqldelight/databases/4.db index f2e7eb964f4cd9384e8858a2b768500082887ce0..f51605ff2a20879c912fdaf404cdbe2115a399e6 100644 GIT binary patch delta 49 zcmZojXh@il#TLWB7sG#WW8oWKuBIeLc5!88##Zjl1$@?wTna9(ZjPZoAqs|@SM!G| F004N=4kZ8p delta 39 vcmZojXh@il#g@jvm&Si^v!K8?UaqDHMs{&!WyY5B&1rnrjGI^UhbjO7_l671 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 8cebfbe5e2..bffec1261b 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 @@ -7,7 +7,7 @@ CREATE TABLE SessionData ( slidingSyncProxy TEXT, loginTimestamp INTEGER, oidcData TEXT, - isTokenValid INTEGER NOT NULL, + isTokenValid INTEGER NOT NULL DEFAULT 1, loginType TEXT ); From 9bacf9df8efbe0928039d8e38538fb3879a55aae Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 14:52:04 +0200 Subject: [PATCH 014/281] Add DB versions to test all migrations --- .../impl/src/main/sqldelight/databases/1.db | Bin 0 -> 12288 bytes .../impl/src/main/sqldelight/databases/2.db | Bin 0 -> 12288 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 libraries/session-storage/impl/src/main/sqldelight/databases/1.db create mode 100644 libraries/session-storage/impl/src/main/sqldelight/databases/2.db diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/1.db b/libraries/session-storage/impl/src/main/sqldelight/databases/1.db new file mode 100644 index 0000000000000000000000000000000000000000..24a9b98f93ff4ccef492444363d87a22d1621912 GIT binary patch literal 12288 zcmeI#O-sWt7zglVib`QGw_O9hD2gEB-Kss5Rh?}I?o?`Jplg_&P*|Y$42tWV=5P$##AOHafKmY;|fIySLo1puBU8nEIMevkxm*;t$ zr8hwl)E-;cj%j!%^NcIoWVHf2)R>HU&V>~*&wTXQ-Mwrh+h#+@x;30J8<^vha#q9_ zamfG9t7<_Q%ALLJg{QwiQecIMHsf;Z1s5Zc{I@Po;wVm^-BlV6MYdd( zbF`z2)?VMzDT&jFFZ0(#-sb&aUSwtN*Usmk)e?2DKTw`+)%8?|fB*y_009U<00Izz s00bZa0SG`~TLmyQ_4ndj5P$##AOHafKmY;|fB*y_0D)5A8;d1pbN~PV literal 0 HcmV?d00001 diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/2.db b/libraries/session-storage/impl/src/main/sqldelight/databases/2.db new file mode 100644 index 0000000000000000000000000000000000000000..8d5a4188bded074de21baf94f331124773276a63 GIT binary patch literal 12288 zcmeI#O-sWt7zgllippRww_O9fD2O29-Kjy!>YQB%?lfYXZNaY9v>V%XegQwqzJlM% zqe;uKim?0mKajLfpS+M?Pw726G8J?bt0_=jC!qWhtWcT9+#<(St67jP>(%%wDX`_-*ITyweMT*fVSD-G#eF& zx%9=~ae0mVzTVl3UqtkKnY^sTNQq?fQ_nw7;;GPE&xPu%@V|LUC<7Udd&|i0s(7)? z#^iIucq}7N>IG6heWiBCV|UCgpInv>8g|nnG77{Zc? Date: Thu, 12 Oct 2023 14:59:06 +0200 Subject: [PATCH 015/281] Add some comment in the files manipulating the session DB --- .../android/libraries/matrix/session/SessionData.sq | 7 +++++++ .../impl/src/main/sqldelight/migrations/0.sqm | 3 +++ .../impl/src/main/sqldelight/migrations/1.sqm | 2 ++ .../impl/src/main/sqldelight/migrations/2.sqm | 2 ++ .../impl/src/main/sqldelight/migrations/3.sqm | 2 ++ 5 files changed, 16 insertions(+) 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 bffec1261b..09f2972bca 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 @@ -1,3 +1,7 @@ +------------------------------------ +-- Current version of the DB: 4 -- +------------------------------------ + CREATE TABLE SessionData ( userId TEXT NOT NULL PRIMARY KEY, deviceId TEXT NOT NULL, @@ -5,8 +9,11 @@ CREATE TABLE SessionData ( refreshToken TEXT, homeserverUrl TEXT NOT NULL, slidingSyncProxy TEXT, + -- added in version 2 loginTimestamp INTEGER, + -- added in version 3 oidcData TEXT, + -- added in version 4 isTokenValid INTEGER NOT NULL DEFAULT 1, loginType TEXT ); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/0.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/0.sqm index 396a8f28dd..4577105e3d 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/migrations/0.sqm +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/0.sqm @@ -1,3 +1,6 @@ +-- This file is not striclty necessary, since the first +-- version of the DB is 1, so we will never migrate from 0 + CREATE TABLE SessionData ( userId TEXT NOT NULL PRIMARY KEY, deviceId TEXT NOT NULL, diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/1.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/1.sqm index 3ee7762585..845fabc321 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/migrations/1.sqm +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/1.sqm @@ -1 +1,3 @@ +-- Migrate DB from version 1 + ALTER TABLE SessionData ADD COLUMN loginTimestamp INTEGER; diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/2.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/2.sqm index 9fc7f2fdaa..0af4cf8d2b 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/migrations/2.sqm +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/2.sqm @@ -1 +1,3 @@ +-- Migrate DB from version 2 + ALTER TABLE SessionData ADD COLUMN oidcData TEXT; diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm index c4d0743ff5..eef6eb5efb 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/3.sqm @@ -1,2 +1,4 @@ +-- Migrate DB from version 3 + ALTER TABLE SessionData ADD COLUMN isTokenValid INTEGER NOT NULL DEFAULT 1; ALTER TABLE SessionData ADD COLUMN loginType TEXT; From c5fbb9d2977980b334804c904e5091f22d324105 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Oct 2023 17:10:21 +0200 Subject: [PATCH 016/281] Split Konsist test into multiple files --- .../android/app/KonsistArchitectureTest.kt | 46 ++++++ .../android/app/KonsistClassNameTest.kt | 35 +++++ .../android/app/KonsistComposableTest.kt | 62 ++++++++ .../element/android/app/KonsistFieldTest.kt | 36 +++++ .../element/android/app/KonsistPreviewTest.kt | 38 +++++ .../io/element/android/app/KonsistTest.kt | 139 ------------------ .../io/element/android/app/KonsistTestTest.kt | 37 +++++ 7 files changed, 254 insertions(+), 139 deletions(-) create mode 100644 app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt create mode 100644 app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt create mode 100644 app/src/test/kotlin/io/element/android/app/KonsistComposableTest.kt create mode 100644 app/src/test/kotlin/io/element/android/app/KonsistFieldTest.kt create mode 100644 app/src/test/kotlin/io/element/android/app/KonsistPreviewTest.kt delete mode 100644 app/src/test/kotlin/io/element/android/app/KonsistTest.kt create mode 100644 app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt diff --git a/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt new file mode 100644 index 0000000000..e3945b700b --- /dev/null +++ b/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt @@ -0,0 +1,46 @@ +/* + * 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.app + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.constructors +import com.lemonappdev.konsist.api.ext.list.parameters +import com.lemonappdev.konsist.api.ext.list.withNameEndingWith +import com.lemonappdev.konsist.api.ext.list.withoutName +import com.lemonappdev.konsist.api.verify.assertTrue +import org.junit.Test + +class KonsistArchitectureTest { + @Test + fun `Data class state MUST not have default value`() { + Konsist + .scopeFromProject() + .classes() + .withNameEndingWith("State") + .withoutName( + "CameraPositionState", + ) + .constructors + .parameters + .assertTrue { parameterDeclaration -> + parameterDeclaration.defaultValue == null && + // Using parameterDeclaration.defaultValue == null is not enough apparently, + // Also check that the text does not contain an equal sign + parameterDeclaration.text.contains("=").not() + } + } +} diff --git a/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt new file mode 100644 index 0000000000..2a23bfc978 --- /dev/null +++ b/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.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.app + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.withAllParentsOf +import com.lemonappdev.konsist.api.verify.assertTrue +import io.element.android.libraries.architecture.Presenter +import org.junit.Test + +class KonsistClassNameTest { + @Test + fun `Classes extending 'Presenter' should have 'Presenter' suffix`() { + Konsist.scopeFromProject() + .classes() + .withAllParentsOf(Presenter::class) + .assertTrue { + it.name.endsWith("Presenter") + } + } +} diff --git a/app/src/test/kotlin/io/element/android/app/KonsistComposableTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistComposableTest.kt new file mode 100644 index 0000000000..c09f3c07a8 --- /dev/null +++ b/app/src/test/kotlin/io/element/android/app/KonsistComposableTest.kt @@ -0,0 +1,62 @@ +/* + * 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.app + +import androidx.compose.runtime.Composable +import com.lemonappdev.konsist.api.KoModifier +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutModifier +import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf +import com.lemonappdev.konsist.api.ext.list.withTopLevel +import com.lemonappdev.konsist.api.ext.list.withoutName +import com.lemonappdev.konsist.api.ext.list.withoutNameEndingWith +import com.lemonappdev.konsist.api.verify.assertTrue +import org.junit.Test + +class KonsistComposableTest { + @Test + fun `Top level function with '@Composable' annotation starting with a upper case should be placed in a file with the same name`() { + Konsist + .scopeFromProject() + .functions() + .withTopLevel() + .withoutModifier(KoModifier.PRIVATE) + .withoutNameEndingWith("Preview") + .withAllAnnotationsOf(Composable::class) + .withoutName( + // Add some exceptions... + "OutlinedButton", + "TextButton", + "SimpleAlertDialogContent", + ) + .assertTrue( + additionalMessage = + """ + Please check the filename. It should match the top level Composable function. If the filename is correct: + - consider making the Composable private or moving it to its own file + - at last resort, you can add an exception in the Konsist test + """.trimIndent() + ) { + if (it.name.first().isLowerCase()) { + true + } else { + val fileName = it.containingFile.name.removeSuffix(".kt") + fileName == it.name + } + } + } +} diff --git a/app/src/test/kotlin/io/element/android/app/KonsistFieldTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistFieldTest.kt new file mode 100644 index 0000000000..13d5c9949b --- /dev/null +++ b/app/src/test/kotlin/io/element/android/app/KonsistFieldTest.kt @@ -0,0 +1,36 @@ +/* + * 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.app + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.properties +import com.lemonappdev.konsist.api.verify.assertFalse +import org.junit.Test + +class KonsistFieldTest { + @Test + fun `no field should have 'm' prefix`() { + Konsist + .scopeFromProject() + .classes() + .properties() + .assertFalse { + val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false + it.name.startsWith('m') && secondCharacterIsUppercase + } + } +} diff --git a/app/src/test/kotlin/io/element/android/app/KonsistPreviewTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistPreviewTest.kt new file mode 100644 index 0000000000..f9bcf620d0 --- /dev/null +++ b/app/src/test/kotlin/io/element/android/app/KonsistPreviewTest.kt @@ -0,0 +1,38 @@ +/* + * 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.app + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf +import com.lemonappdev.konsist.api.verify.assertTrue +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import org.junit.Test + +class KonsistPreviewTest { + @Test + fun `Functions with '@PreviewsDayNight' annotation should have 'Preview' suffix`() { + Konsist + .scopeFromProject() + .functions() + .withAllAnnotationsOf(PreviewsDayNight::class) + .assertTrue { + it.hasNameEndingWith("Preview") && + it.hasNameEndingWith("LightPreview").not() && + it.hasNameEndingWith("DarkPreview").not() + } + } +} diff --git a/app/src/test/kotlin/io/element/android/app/KonsistTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistTest.kt deleted file mode 100644 index 25367e8c5a..0000000000 --- a/app/src/test/kotlin/io/element/android/app/KonsistTest.kt +++ /dev/null @@ -1,139 +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.app - -import androidx.compose.runtime.Composable -import com.lemonappdev.konsist.api.KoModifier -import com.lemonappdev.konsist.api.Konsist -import com.lemonappdev.konsist.api.ext.list.constructors -import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutModifier -import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier -import com.lemonappdev.konsist.api.ext.list.parameters -import com.lemonappdev.konsist.api.ext.list.properties -import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf -import com.lemonappdev.konsist.api.ext.list.withAllParentsOf -import com.lemonappdev.konsist.api.ext.list.withNameEndingWith -import com.lemonappdev.konsist.api.ext.list.withReturnType -import com.lemonappdev.konsist.api.ext.list.withTopLevel -import com.lemonappdev.konsist.api.ext.list.withoutName -import com.lemonappdev.konsist.api.ext.list.withoutNameEndingWith -import com.lemonappdev.konsist.api.verify.assertFalse -import com.lemonappdev.konsist.api.verify.assertTrue -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import org.junit.Test - -class KonsistTest { - - @Test - fun `Classes extending 'Presenter' should have 'Presenter' suffix`() { - Konsist.scopeFromProject() - .classes() - .withAllParentsOf(Presenter::class) - .assertTrue { - it.name.endsWith("Presenter") - } - } - - @Test - fun `Functions with '@PreviewsDayNight' annotation should have 'Preview' suffix`() { - Konsist - .scopeFromProject() - .functions() - .withAllAnnotationsOf(PreviewsDayNight::class) - .assertTrue { - it.hasNameEndingWith("Preview") && - it.hasNameEndingWith("LightPreview").not() && - it.hasNameEndingWith("DarkPreview").not() - } - } - - @Test - fun `Top level function with '@Composable' annotation starting with a upper case should be placed in a file with the same name`() { - Konsist - .scopeFromProject() - .functions() - .withTopLevel() - .withoutModifier(KoModifier.PRIVATE) - .withoutNameEndingWith("Preview") - .withAllAnnotationsOf(Composable::class) - .withoutName( - // Add some exceptions... - "OutlinedButton", - "TextButton", - "SimpleAlertDialogContent", - ) - .assertTrue( - additionalMessage = - """ - Please check the filename. It should match the top level Composable function. If the filename is correct: - - consider making the Composable private or moving it to its own file - - at last resort, you can add an exception in the Konsist test - """.trimIndent() - ) { - if (it.name.first().isLowerCase()) { - true - } else { - val fileName = it.containingFile.name.removeSuffix(".kt") - fileName == it.name - } - } - } - - @Test - fun `Data class state MUST not have default value`() { - Konsist - .scopeFromProject() - .classes() - .withNameEndingWith("State") - .withoutName( - "CameraPositionState", - ) - .constructors - .parameters - .assertTrue { parameterDeclaration -> - parameterDeclaration.defaultValue == null && - // Using parameterDeclaration.defaultValue == null is not enough apparently, - // Also check that the text does not contain an equal sign - parameterDeclaration.text.contains("=").not() - } - } - - @Test - fun `Function which creates Presenter in test MUST be named 'createPresenterName'`() { - Konsist - .scopeFromTest() - .functions() - .withReturnType { it.name.endsWith("Presenter") } - .withoutOverrideModifier() - .assertTrue { functionDeclaration -> - functionDeclaration.name == "create${functionDeclaration.returnType?.name}" - } - } - - @Test - fun `no field should have 'm' prefix`() { - Konsist - .scopeFromProject() - .classes() - .properties() - .assertFalse { - val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false - it.name.startsWith('m') && secondCharacterIsUppercase - } - } -} diff --git a/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt new file mode 100644 index 0000000000..b4f24eeccb --- /dev/null +++ b/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt @@ -0,0 +1,37 @@ +/* + * 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.app + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier +import com.lemonappdev.konsist.api.ext.list.withReturnType +import com.lemonappdev.konsist.api.verify.assertTrue +import org.junit.Test + +class KonsistTestTest { + @Test + fun `Function which creates Presenter in test MUST be named 'createPresenterName'`() { + Konsist + .scopeFromTest() + .functions() + .withReturnType { it.name.endsWith("Presenter") } + .withoutOverrideModifier() + .assertTrue { functionDeclaration -> + functionDeclaration.name == "create${functionDeclaration.returnType?.name}" + } + } +} From 65b83bf26692dc10bd3725cca45b167e3a1fae97 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Oct 2023 17:11:36 +0200 Subject: [PATCH 017/281] Konsist: add test to check Node class name. --- .../io/element/android/app/KonsistClassNameTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt index 2a23bfc978..3a56e2d3ca 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt +++ b/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt @@ -16,6 +16,7 @@ package io.element.android.app +import com.bumble.appyx.core.node.Node import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.withAllParentsOf import com.lemonappdev.konsist.api.verify.assertTrue @@ -32,4 +33,14 @@ class KonsistClassNameTest { it.name.endsWith("Presenter") } } + + @Test + fun `Classes extending 'Node' should have 'Node' suffix`() { + Konsist.scopeFromProject() + .classes() + .withAllParentsOf(Node::class) + .assertTrue { + it.name.endsWith("Node") + } + } } From 3c41158558b21003f64bd444f78003c30cf3405d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Oct 2023 17:21:11 +0200 Subject: [PATCH 018/281] Use existing `BooleanProvider` --- .../location/api/internal/StaticMapPlaceholder.kt | 11 +++-------- .../libraries/designsystem/utils/BooleanProvider.kt | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt index 2a2a3995fb..f13349f0d8 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt @@ -30,15 +30,15 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.features.location.api.R -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.Text +import io.element.android.libraries.designsystem.utils.BooleanProvider import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -81,7 +81,7 @@ internal fun StaticMapPlaceholder( @PreviewsDayNight @Composable internal fun StaticMapPlaceholderPreview( - @PreviewParameter(BooleanParameterProvider::class) values: Boolean + @PreviewParameter(BooleanProvider::class) values: Boolean ) = ElementPreview { StaticMapPlaceholder( showProgress = values, @@ -91,8 +91,3 @@ internal fun StaticMapPlaceholderPreview( onLoadMapClick = {}, ) } - -internal class BooleanParameterProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf(true, false) -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanProvider.kt index 43d0f9e797..1bc35aed89 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanProvider.kt @@ -20,5 +20,5 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider open class BooleanProvider : PreviewParameterProvider { override val values: Sequence - get() = sequenceOf(false, true) + get() = sequenceOf(true, false) } From 8f8a5746e5cdf06209415fe0036a8bac67fa97f7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Oct 2023 17:33:56 +0200 Subject: [PATCH 019/281] Konsist: add test about PreviewParameterProvider class name and fix existing issues. --- .../android/app/KonsistClassNameTest.kt | 17 +++++++++++ .../designsystem/components/Bloom.kt | 4 +-- .../utils/PairCombinedProvider.kt | 30 ------------------- ...rovider.kt => PermissionsStateProvider.kt} | 2 +- .../permissions/api/PermissionsView.kt | 2 +- 5 files changed, 21 insertions(+), 34 deletions(-) delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/PairCombinedProvider.kt rename libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/{PermissionsViewStateProvider.kt => PermissionsStateProvider.kt} (94%) diff --git a/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt index 3a56e2d3ca..d187816682 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt +++ b/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt @@ -16,6 +16,7 @@ package io.element.android.app +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.bumble.appyx.core.node.Node import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.withAllParentsOf @@ -43,4 +44,20 @@ class KonsistClassNameTest { it.name.endsWith("Node") } } + + @Test + fun `Classes extending 'PreviewParameterProvider' name MUST end with "Provider" and MUST contain provided class name`() { + Konsist.scopeFromProject() + .classes() + .withAllParentsOf(PreviewParameterProvider::class) + .assertTrue { + // Cannot find a better way to get the type of the generic + val providedType = it.text + .substringBefore(">") + .substringAfter("<") + .removeSuffix("?") + .replace(".", "") + it.name.endsWith("Provider") && it.name.contains(providedType) + } + } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt index ff8d4221f6..c44a1140f0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/Bloom.kt @@ -537,7 +537,7 @@ internal fun BloomPreview() { } } -class InitialsColorStateProvider : PreviewParameterProvider { +class InitialsColorIntProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf(0, 1, 2, 3, 4, 5, 6, 7) } @@ -545,7 +545,7 @@ class InitialsColorStateProvider : PreviewParameterProvider { @PreviewsDayNight @Composable @ShowkaseComposable(group = PreviewGroup.Bloom) -internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorStateProvider::class) color: Int) { +internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorIntProvider::class) color: Int) { ElementPreview { val avatarColors = AvatarColorsProvider.provide("$color", ElementTheme.isLightTheme) val bitmap = initialsBitmap(text = "F", backgroundColor = avatarColors.background, textColor = avatarColors.foreground) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/PairCombinedProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/PairCombinedProvider.kt deleted file mode 100644 index 0f53d44e44..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/PairCombinedProvider.kt +++ /dev/null @@ -1,30 +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.utils - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -open class PairCombinedProvider( - private val provider: Pair, PreviewParameterProvider> -) : PreviewParameterProvider> { - override val values: Sequence> - get() = provider.first.values.flatMap { first -> - provider.second.values.map { second -> - first to second - } - } -} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt similarity index 94% rename from libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt rename to libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt index 2009ad0e85..cc59d96b44 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.permissions.api import android.Manifest import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class PermissionsViewStateProvider : PreviewParameterProvider { +open class PermissionsStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPermissionsState(showDialog = true, permission = Manifest.permission.POST_NOTIFICATIONS), diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt index ef15ab561d..9e2413fff9 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt @@ -57,7 +57,7 @@ private fun String.toDialogContent(): String { @PreviewsDayNight @Composable -internal fun PermissionsViewPreview(@PreviewParameter(PermissionsViewStateProvider::class) state: PermissionsState) = ElementPreview { +internal fun PermissionsViewPreview(@PreviewParameter(PermissionsStateProvider::class) state: PermissionsState) = ElementPreview { PermissionsView( state = state, ) From 7389748f3ed5bc19cb434b92eb01705936e4201b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 10:04:42 +0200 Subject: [PATCH 020/281] Konsist: add rule obout test class name, and fix existing issue --- .../kotlin/io/element/android/app/KonsistTestTest.kt | 10 ++++++++++ ...tDataParserTests.kt => CallIntentDataParserTest.kt} | 2 +- .../{ErrorFormatterTests.kt => ErrorFormatterTest.kt} | 2 +- .../core/extensions/{ResultTests.kt => ResultTest.kt} | 2 +- ...Tests.kt => DefaultRoomLastMessageFormatterTest.kt} | 2 +- .../{AuthErrorCodeTests.kt => AuthErrorCodeTest.kt} | 2 +- ...rixToConverterTests.kt => MatrixToConverterTest.kt} | 2 +- ...ermalinkBuilderTests.kt => PermalinkBuilderTest.kt} | 2 +- ...{PermalinkParserTests.kt => PermalinkParserTest.kt} | 2 +- ...gTests.kt => AuthenticationExceptionMappingTest.kt} | 2 +- .../{PickerTypeTests.kt => PickerTypeTest.kt} | 2 +- 11 files changed, 20 insertions(+), 10 deletions(-) rename features/call/src/test/kotlin/io/element/android/features/call/{CallIntentDataParserTests.kt => CallIntentDataParserTest.kt} (99%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/{ErrorFormatterTests.kt => ErrorFormatterTest.kt} (98%) rename libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/{ResultTests.kt => ResultTest.kt} (99%) rename libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/{DefaultRoomLastMessageFormatterTests.kt => DefaultRoomLastMessageFormatterTest.kt} (99%) rename libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/{AuthErrorCodeTests.kt => AuthErrorCodeTest.kt} (98%) rename libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/{MatrixToConverterTests.kt => MatrixToConverterTest.kt} (98%) rename libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/{PermalinkBuilderTests.kt => PermalinkBuilderTest.kt} (99%) rename libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/{PermalinkParserTests.kt => PermalinkParserTest.kt} (99%) rename libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/{AuthenticationExceptionMappingTests.kt => AuthenticationExceptionMappingTest.kt} (99%) rename libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/{PickerTypeTests.kt => PickerTypeTest.kt} (99%) diff --git a/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt index b4f24eeccb..3ed6bd2026 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt +++ b/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt @@ -18,11 +18,21 @@ package io.element.android.app import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier +import com.lemonappdev.konsist.api.ext.list.withFunction import com.lemonappdev.konsist.api.ext.list.withReturnType import com.lemonappdev.konsist.api.verify.assertTrue import org.junit.Test class KonsistTestTest { + @Test + fun `Classes name containing @Test must end with 'Test''`() { + Konsist + .scopeFromTest() + .classes() + .withFunction { it.hasAnnotationOf(Test::class) } + .assertTrue { it.name.endsWith("Test") } + } + @Test fun `Function which creates Presenter in test MUST be named 'createPresenterName'`() { Konsist diff --git a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt similarity index 99% rename from features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt rename to features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt index f23a5fb43c..ae82767f45 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt @@ -23,7 +23,7 @@ import org.robolectric.RobolectricTestRunner import java.net.URLEncoder @RunWith(RobolectricTestRunner::class) -class CallIntentDataParserTests { +class CallIntentDataParserTest { private val callIntentDataParser = CallIntentDataParser() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTests.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt similarity index 98% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTests.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt index c1d7e5bb6c..17c6ce3e4a 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTests.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/error/ErrorFormatterTest.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.ui.strings.CommonStrings import org.junit.Test -class ErrorFormatterTests { +class ErrorFormatterTest { // region loginError @Test diff --git a/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTests.kt b/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTest.kt similarity index 99% rename from libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTests.kt rename to libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTest.kt index de5703b090..7f83c9de8f 100644 --- a/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTests.kt +++ b/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTest.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.core.extensions import com.google.common.truth.Truth.assertThat import org.junit.Test -class ResultTests { +class ResultTest { @Test fun testFlatMap() { diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt similarity index 99% rename from libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt rename to libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt index c906952bbb..4c26fcb3c7 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTests.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt @@ -61,7 +61,7 @@ import org.robolectric.annotation.Config @Suppress("LargeClass") @RunWith(RobolectricTestRunner::class) -class DefaultRoomLastMessageFormatterTests { +class DefaultRoomLastMessageFormatterTest { private lateinit var context: Context private lateinit var fakeMatrixClient: FakeMatrixClient diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/AuthErrorCodeTests.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/AuthErrorCodeTest.kt similarity index 98% rename from libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/AuthErrorCodeTests.kt rename to libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/AuthErrorCodeTest.kt index b3ccf5264d..5f987ad725 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/AuthErrorCodeTests.kt +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/auth/AuthErrorCodeTest.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.api.auth import com.google.common.truth.Truth.assertThat import org.junit.Test -class AuthErrorCodeTests { +class AuthErrorCodeTest { @Test fun `errorCode finds UNKNOWN code`() { diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTests.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTest.kt similarity index 98% rename from libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTests.kt rename to libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTest.kt index ef0a586fba..d3d928c189 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTests.kt +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTest.kt @@ -23,7 +23,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class MatrixToConverterTests { +class MatrixToConverterTest { @Test fun `converting a matrix-to url does nothing`() { diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTests.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTest.kt similarity index 99% rename from libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTests.kt rename to libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTest.kt index 0a9a03cabb..282f58ab21 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTests.kt +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTest.kt @@ -23,7 +23,7 @@ import io.element.android.tests.testutils.assertThrowsInDebug import io.element.android.tests.testutils.isInDebug import org.junit.Test -class PermalinkBuilderTests { +class PermalinkBuilderTest { fun `building a permalink for an invalid user id throws when verifying the id`() { assertThrowsInDebug { diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTests.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt similarity index 99% rename from libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTests.kt rename to libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt index 74797083c4..d55b6aebbe 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTests.kt +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt @@ -22,7 +22,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class PermalinkParserTests { +class PermalinkParserTest { @Test fun `parsing an invalid url returns a fallback link`() { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt similarity index 99% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt index 2a8dd5c122..b0c02c9dcd 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTests.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.auth.AuthenticationException import org.junit.Test import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException -class AuthenticationExceptionMappingTests { +class AuthenticationExceptionMappingTest { @Test fun `mapping an exception with no message returns 'Unknown error' message`() { diff --git a/libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt b/libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTest.kt similarity index 99% rename from libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt rename to libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTest.kt index 4f17081f8b..d759dc3020 100644 --- a/libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTests.kt +++ b/libraries/mediapickers/api/src/test/kotlin/io/element/android/libraries/mediapickers/PickerTypeTest.kt @@ -26,7 +26,7 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class PickerTypeTests { +class PickerTypeTest { @Test fun `ImageAndVideo - assert types`() { From 634095c43abbe45f57b3426f3596381bad5e38f2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 10:43:35 +0200 Subject: [PATCH 021/281] Konsist: add test for Events --- .../io/element/android/app/KonsistArchitectureTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt b/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt index e3945b700b..17e9710653 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt +++ b/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt @@ -18,9 +18,11 @@ package io.element.android.app import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.constructors +import com.lemonappdev.konsist.api.ext.list.modifierprovider.withSealedModifier import com.lemonappdev.konsist.api.ext.list.parameters import com.lemonappdev.konsist.api.ext.list.withNameEndingWith import com.lemonappdev.konsist.api.ext.list.withoutName +import com.lemonappdev.konsist.api.verify.assertEmpty import com.lemonappdev.konsist.api.verify.assertTrue import org.junit.Test @@ -43,4 +45,13 @@ class KonsistArchitectureTest { parameterDeclaration.text.contains("=").not() } } + + @Test + fun `Events MUST be sealed interface`() { + Konsist.scopeFromProject() + .classes() + .withSealedModifier() + .withNameEndingWith("Events") + .assertEmpty(additionalMessage = "Events class MUST be sealed interface") + } } From 12996e825353c71f1a5f59d650d500721fcb9849 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 15:48:13 +0200 Subject: [PATCH 022/281] Improve comment. --- .../android/libraries/matrix/session/SessionData.sq | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 09f2972bca..d6d16cb6e2 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 @@ -1,6 +1,12 @@ ------------------------------------- --- Current version of the DB: 4 -- ------------------------------------- +-------------------------------------------------------------------- +-- Current version of the DB is the highest value of filename +-- in the folder `sqldelight/databases`. +-- +-- When upgrading the schema, you have to create a file .sqm in the +-- `sqldelight/databases` folder and run the following task to +-- generate a .db file using the latest schema +-- > ./gradlew generateDebugSessionDatabaseSchema +-------------------------------------------------------------------- CREATE TABLE SessionData ( userId TEXT NOT NULL PRIMARY KEY, From c3bc44bfecb1174008405903c311aaf893f50223 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 12:10:20 +0200 Subject: [PATCH 023/281] Ensure screen does not turn off when playing a video (#1519) --- changelog.d/1519.bugfix | 1 + .../impl/media/local/LocalMediaView.kt | 6 ++++ .../impl/media/local/LocalMediaViewState.kt | 1 + .../designsystem/utils/KeepScreenOn.kt | 36 +++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 changelog.d/1519.bugfix create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/KeepScreenOn.kt diff --git a/changelog.d/1519.bugfix b/changelog.d/1519.bugfix new file mode 100644 index 0000000000..35de17de6b --- /dev/null +++ b/changelog.d/1519.bugfix @@ -0,0 +1 @@ +Ensure screen does not turn off when playing a video diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 3ca534c310..4348b83a95 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -66,6 +66,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.theme.ElementTheme import me.saket.telephoto.zoomable.ZoomSpec @@ -152,6 +153,10 @@ private fun MediaVideoView( override fun onRenderedFirstFrame() { localMediaViewState.isReady = true } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + localMediaViewState.isPlaying = isPlaying + } } val exoPlayer = remember { ExoPlayerWrapper.create(context) @@ -168,6 +173,7 @@ private fun MediaVideoView( } else { exoPlayer.setMediaItems(emptyList()) } + KeepScreenOn(localMediaViewState.isPlaying) AndroidView( factory = { PlayerView(context).apply { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaViewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaViewState.kt index e009c3f6cc..d5af7a78e1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaViewState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaViewState.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.setValue @Stable class LocalMediaViewState { var isReady: Boolean by mutableStateOf(false) + var isPlaying: Boolean by mutableStateOf(false) } @Composable diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/KeepScreenOn.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/KeepScreenOn.kt new file mode 100644 index 0000000000..a9cf913e2a --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/KeepScreenOn.kt @@ -0,0 +1,36 @@ +/* + * 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.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalView + +@Composable +fun KeepScreenOn( + keepScreenOn: Boolean = true +) { + if (keepScreenOn) { + val currentView = LocalView.current + DisposableEffect(Unit) { + currentView.keepScreenOn = true + onDispose { + currentView.keepScreenOn = false + } + } + } +} From 087142224c1e4afc2667e9492cef64d4f2caa6eb Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 12 Oct 2023 16:19:24 +0200 Subject: [PATCH 024/281] Pin setup: PR review --- .../android/appnav/LoggedInFlowNode.kt | 2 +- .../android/features/pin/api/PinEntryPoint.kt | 20 ++----------------- features/pin/impl/build.gradle.kts | 2 -- .../features/pin/impl/DefaultPinEntryPoint.kt | 17 ++-------------- 4 files changed, 5 insertions(+), 36 deletions(-) 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 d1c975f5b6..ebb9fee54a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -218,7 +218,7 @@ class LoggedInFlowNode @AssistedInject constructor( createNode(buildContext) } NavTarget.LockPermanent -> { - pinEntryPoint.nodeBuilder(this, buildContext).build() + pinEntryPoint.createNode(this, buildContext) } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt index df0bf255f4..1fe3caf574 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt +++ b/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt @@ -16,22 +16,6 @@ package io.element.android.features.pin.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 PinEntryPoint : FeatureEntryPoint { - - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder - - interface NodeBuilder { - fun callback(callback: Callback): NodeBuilder - fun build(): Node - } - - interface Callback : Plugin { - // Add your callbacks - } -} +import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +interface PinEntryPoint : SimpleFeatureEntryPoint diff --git a/features/pin/impl/build.gradle.kts b/features/pin/impl/build.gradle.kts index 0d115ac46c..6cfc9fce11 100644 --- a/features/pin/impl/build.gradle.kts +++ b/features/pin/impl/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed -@Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt index 1dfba1a4cf..920691cad2 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt @@ -18,7 +18,6 @@ package io.element.android.features.pin.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.pin.api.PinEntryPoint import io.element.android.libraries.architecture.createNode @@ -28,19 +27,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPinEntryPoint @Inject constructor() : PinEntryPoint { - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PinEntryPoint.NodeBuilder { - val plugins = ArrayList() - - return object : PinEntryPoint.NodeBuilder { - - override fun callback(callback: PinEntryPoint.Callback): PinEntryPoint.NodeBuilder { - plugins += callback - return this - } - - override fun build(): Node { - return parentNode.createNode(buildContext, plugins) - } - } + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) } } From 12404fab7809e182347c74b201aa807d0558e485 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 12 Oct 2023 16:17:18 +0100 Subject: [PATCH 025/281] [Voice messages] Add voice recording UI (#1546) --------- Co-authored-by: ElementBot --- .../impl/send/SendLocationPresenterTest.kt | 2 +- .../messages/api/MessageComposerContext.kt | 2 +- .../messages/impl/MessagesPresenter.kt | 15 ++- .../features/messages/impl/MessagesState.kt | 3 + .../messages/impl/MessagesStateProvider.kt | 5 +- .../features/messages/impl/MessagesView.kt | 2 + .../MessageComposerContextImpl.kt | 2 +- .../messagecomposer/MessageComposerEvents.kt | 4 +- .../MessageComposerPresenter.kt | 4 +- .../messagecomposer/MessageComposerState.kt | 2 +- .../MessageComposerStateProvider.kt | 2 +- .../messagecomposer/MessageComposerView.kt | 43 ++++++- .../VoiceMessageComposerEvents.kt | 25 ++++ .../VoiceMessageComposerPresenter.kt | 64 ++++++++++ .../VoiceMessageComposerState.kt | 27 +++++ .../VoiceMessageComposerStateProvider.kt | 34 ++++++ .../messages/MessagesPresenterTest.kt | 6 +- .../MessageComposerPresenterTest.kt | 4 +- .../VoiceMessageComposerPresenterTest.kt | 89 ++++++++++++++ .../test/MessageComposerContextFake.kt | 2 +- libraries/textcomposer/impl/build.gradle.kts | 4 + .../libraries/textcomposer/TextComposer.kt | 107 ++++++++++++++--- .../textcomposer/components/RecordButton.kt | 108 +++++++++++++++++ .../components/RecordingProgress.kt | 74 ++++++++++++ .../textcomposer/components/SendButton.kt | 2 +- .../components/TextInputRoundedCornerShape.kt | 2 +- .../textcomposer/{ => model}/Message.kt | 2 +- .../{ => model}/MessageComposerMode.kt | 2 +- .../textcomposer/model/PressEvent.kt | 23 ++++ .../textcomposer/model/VoiceMessageState.kt | 22 ++++ .../textcomposer/utils/PressState.kt | 31 +++++ .../textcomposer/utils/PressStateEffects.kt | 47 ++++++++ .../textcomposer/utils/PressStateHolder.kt | 101 ++++++++++++++++ .../utils/PressStateHolderTest.kt | 111 ++++++++++++++++++ .../src/main/res/values/localazy.xml | 4 + ...poserView-D-4_4_null_0,NEXUS_5,1.0,en].png | 4 +- ...poserView-N-4_5_null_0,NEXUS_5,1.0,en].png | 4 +- ...ViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png | 3 + ...ViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png | 3 + ...sageView-D-6_6_null_0,NEXUS_5,1.0,en].png} | 0 ...sageView-D-6_6_null_1,NEXUS_5,1.0,en].png} | 0 ...sageView-D-6_6_null_2,NEXUS_5,1.0,en].png} | 0 ...sageView-D-6_6_null_3,NEXUS_5,1.0,en].png} | 0 ...sageView-D-6_6_null_4,NEXUS_5,1.0,en].png} | 0 ...sageView-D-6_6_null_5,NEXUS_5,1.0,en].png} | 0 ...sageView-N-6_7_null_0,NEXUS_5,1.0,en].png} | 0 ...sageView-N-6_7_null_1,NEXUS_5,1.0,en].png} | 0 ...sageView-N-6_7_null_2,NEXUS_5,1.0,en].png} | 0 ...sageView-N-6_7_null_3,NEXUS_5,1.0,en].png} | 0 ...sageView-N-6_7_null_4,NEXUS_5,1.0,en].png} | 0 ...sageView-N-6_7_null_5,NEXUS_5,1.0,en].png} | 0 ...jiPicker-D-25_25_null,NEXUS_5,1.0,en].png} | 0 ...jiPicker-N-25_26_null,NEXUS_5,1.0,en].png} | 0 ...ioView-D-26_26_null_0,NEXUS_5,1.0,en].png} | 0 ...ioView-D-26_26_null_1,NEXUS_5,1.0,en].png} | 0 ...ioView-D-26_26_null_2,NEXUS_5,1.0,en].png} | 0 ...ioView-N-26_27_null_0,NEXUS_5,1.0,en].png} | 0 ...ioView-N-26_27_null_1,NEXUS_5,1.0,en].png} | 0 ...ioView-N-26_27_null_2,NEXUS_5,1.0,en].png} | 0 ...ptedView-D-27_27_null,NEXUS_5,1.0,en].png} | 0 ...ptedView-N-27_28_null,NEXUS_5,1.0,en].png} | 0 ...leView-D-28_28_null_0,NEXUS_5,1.0,en].png} | 0 ...leView-D-28_28_null_1,NEXUS_5,1.0,en].png} | 0 ...leView-D-28_28_null_2,NEXUS_5,1.0,en].png} | 0 ...leView-N-28_29_null_0,NEXUS_5,1.0,en].png} | 0 ...leView-N-28_29_null_1,NEXUS_5,1.0,en].png} | 0 ...leView-N-28_29_null_2,NEXUS_5,1.0,en].png} | 0 ...geView-D-29_29_null_0,NEXUS_5,1.0,en].png} | 0 ...geView-D-29_29_null_1,NEXUS_5,1.0,en].png} | 0 ...geView-D-29_29_null_2,NEXUS_5,1.0,en].png} | 0 ...geView-N-29_30_null_0,NEXUS_5,1.0,en].png} | 0 ...geView-N-29_30_null_1,NEXUS_5,1.0,en].png} | 0 ...geView-N-29_30_null_2,NEXUS_5,1.0,en].png} | 0 ...tiveView-D-30_30_null,NEXUS_5,1.0,en].png} | 0 ...tiveView-N-30_31_null,NEXUS_5,1.0,en].png} | 0 ...onView-D-31_31_null_0,NEXUS_5,1.0,en].png} | 0 ...onView-D-31_31_null_1,NEXUS_5,1.0,en].png} | 0 ...onView-N-31_32_null_0,NEXUS_5,1.0,en].png} | 0 ...onView-N-31_32_null_1,NEXUS_5,1.0,en].png} | 0 ...orView-D-33_33_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-D-33_33_null_1,NEXUS_5,1.0,en].png} | 0 ...orView-N-33_34_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-N-33_34_null_1,NEXUS_5,1.0,en].png} | 0 ...llView-D-32_32_null_0,NEXUS_5,1.0,en].png} | 0 ...llView-D-32_32_null_1,NEXUS_5,1.0,en].png} | 0 ...llView-N-32_33_null_0,NEXUS_5,1.0,en].png} | 0 ...llView-N-32_33_null_1,NEXUS_5,1.0,en].png} | 0 ...ctedView-D-34_34_null,NEXUS_5,1.0,en].png} | 0 ...ctedView-N-34_35_null,NEXUS_5,1.0,en].png} | 0 ...tateView-D-35_35_null,NEXUS_5,1.0,en].png} | 0 ...tateView-N-35_36_null,NEXUS_5,1.0,en].png} | 0 ...xtView-D-36_36_null_0,NEXUS_5,1.0,en].png} | 0 ...xtView-D-36_36_null_1,NEXUS_5,1.0,en].png} | 0 ...xtView-D-36_36_null_2,NEXUS_5,1.0,en].png} | 0 ...xtView-D-36_36_null_3,NEXUS_5,1.0,en].png} | 0 ...xtView-D-36_36_null_4,NEXUS_5,1.0,en].png} | 0 ...xtView-D-36_36_null_5,NEXUS_5,1.0,en].png} | 0 ...xtView-N-36_37_null_0,NEXUS_5,1.0,en].png} | 0 ...xtView-N-36_37_null_1,NEXUS_5,1.0,en].png} | 0 ...xtView-N-36_37_null_2,NEXUS_5,1.0,en].png} | 0 ...xtView-N-36_37_null_3,NEXUS_5,1.0,en].png} | 0 ...xtView-N-36_37_null_4,NEXUS_5,1.0,en].png} | 0 ...xtView-N-36_37_null_5,NEXUS_5,1.0,en].png} | 0 ...nownView-D-37_37_null,NEXUS_5,1.0,en].png} | 0 ...nownView-N-37_38_null,NEXUS_5,1.0,en].png} | 0 ...eoView-D-38_38_null_0,NEXUS_5,1.0,en].png} | 0 ...eoView-D-38_38_null_1,NEXUS_5,1.0,en].png} | 0 ...eoView-D-38_38_null_2,NEXUS_5,1.0,en].png} | 0 ...eoView-N-38_39_null_0,NEXUS_5,1.0,en].png} | 0 ...eoView-N-38_39_null_1,NEXUS_5,1.0,en].png} | 0 ...eoView-N-38_39_null_2,NEXUS_5,1.0,en].png} | 0 ...aderView-D-39_39_null,NEXUS_5,1.0,en].png} | 0 ...aderView-N-39_40_null,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_0,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_1,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_10,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_11,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_12,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_13,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_14,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_15,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_16,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_17,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_18,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_19,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_2,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_20,NEXUS_5,1.0,en].png} | 0 ...ument-D-40_40_null_21,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_3,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_4,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_5,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_6,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_7,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_8,NEXUS_5,1.0,en].png} | 0 ...cument-D-40_40_null_9,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_0,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_1,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_10,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_11,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_12,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_13,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_14,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_15,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_16,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_17,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_18,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_19,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_2,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_20,NEXUS_5,1.0,en].png} | 0 ...ument-N-40_41_null_21,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_3,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_4,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_5,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_6,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_7,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_8,NEXUS_5,1.0,en].png} | 0 ...cument-N-40_41_null_9,NEXUS_5,1.0,en].png} | 0 ...ontent-D-41_41_null_0,NEXUS_5,1.0,en].png} | 0 ...ontent-N-41_42_null_0,NEXUS_5,1.0,en].png} | 0 ...geMenu-D-42_42_null_0,NEXUS_5,1.0,en].png} | 0 ...geMenu-D-42_42_null_1,NEXUS_5,1.0,en].png} | 0 ...geMenu-N-42_43_null_0,NEXUS_5,1.0,en].png} | 0 ...geMenu-N-42_43_null_1,NEXUS_5,1.0,en].png} | 0 ...nnerView-D-43_43_null,NEXUS_5,1.0,en].png} | 0 ...nnerView-N-43_44_null,NEXUS_5,1.0,en].png} | 0 ...orView-D-44_44_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-D-44_44_null_1,NEXUS_5,1.0,en].png} | 0 ...orView-N-44_45_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-N-44_45_null_1,NEXUS_5,1.0,en].png} | 0 ...ndicator-D-45_45_null,NEXUS_5,1.0,en].png} | 0 ...ndicator-N-45_46_null,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_0,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_1,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-8_8_null_10,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-8_8_null_11,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-8_8_null_12,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-8_8_null_13,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-8_8_null_14,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-8_8_null_15,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_2,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_3,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_4,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_5,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_6,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_7,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_8,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-8_8_null_9,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_0,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_1,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-8_9_null_10,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-8_9_null_11,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-8_9_null_12,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-8_9_null_13,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-8_9_null_14,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-8_9_null_15,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_2,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_3,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_4,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_5,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_6,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_7,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_8,NEXUS_5,1.0,en].png} | 0 ...ntBubble-N-8_9_null_9,NEXUS_5,1.0,en].png} | 0 ...tContainer-D-9_9_null,NEXUS_5,1.0,en].png} | 0 ...Container-N-9_10_null,NEXUS_5,1.0,en].png} | 0 ...onButton-D-11_11_null,NEXUS_5,1.0,en].png} | 0 ...onButton-N-11_12_null,NEXUS_5,1.0,en].png} | 0 ...Button-D-10_10_null_0,NEXUS_5,1.0,en].png} | 0 ...Button-D-10_10_null_1,NEXUS_5,1.0,en].png} | 0 ...Button-D-10_10_null_2,NEXUS_5,1.0,en].png} | 0 ...Button-D-10_10_null_3,NEXUS_5,1.0,en].png} | 0 ...Button-N-10_11_null_0,NEXUS_5,1.0,en].png} | 0 ...Button-N-10_11_null_1,NEXUS_5,1.0,en].png} | 0 ...Button-N-10_11_null_2,NEXUS_5,1.0,en].png} | 0 ...Button-N-10_11_null_3,NEXUS_5,1.0,en].png} | 0 ...aButtons-D-12_12_null,NEXUS_5,1.0,en].png} | 0 ...aButtons-N-12_13_null,NEXUS_5,1.0,en].png} | 0 ...ndicator-D-13_13_null,NEXUS_5,1.0,en].png} | 0 ...ndicator-N-13_14_null,NEXUS_5,1.0,en].png} | 0 ...mpView-D-14_14_null_0,NEXUS_5,1.0,en].png} | 0 ...mpView-D-14_14_null_1,NEXUS_5,1.0,en].png} | 0 ...mpView-D-14_14_null_2,NEXUS_5,1.0,en].png} | 0 ...mpView-D-14_14_null_3,NEXUS_5,1.0,en].png} | 0 ...mpView-N-14_15_null_0,NEXUS_5,1.0,en].png} | 0 ...mpView-N-14_15_null_1,NEXUS_5,1.0,en].png} | 0 ...mpView-N-14_15_null_2,NEXUS_5,1.0,en].png} | 0 ...mpView-N-14_15_null_3,NEXUS_5,1.0,en].png} | 0 ...EventRow-D-15_15_null,NEXUS_5,1.0,en].png} | 0 ...EventRow-N-15_16_null,NEXUS_5,1.0,en].png} | 0 ...estamp-D-17_17_null_0,NEXUS_5,1.0,en].png} | 0 ...estamp-D-17_17_null_1,NEXUS_5,1.0,en].png} | 0 ...estamp-D-17_17_null_2,NEXUS_5,1.0,en].png} | 0 ...estamp-D-17_17_null_3,NEXUS_5,1.0,en].png} | 0 ...estamp-N-17_18_null_0,NEXUS_5,1.0,en].png} | 0 ...estamp-N-17_18_null_1,NEXUS_5,1.0,en].png} | 0 ...estamp-N-17_18_null_2,NEXUS_5,1.0,en].png} | 0 ...estamp-N-17_18_null_3,NEXUS_5,1.0,en].png} | 0 ...eactions-D-18_18_null,NEXUS_5,1.0,en].png} | 0 ...eactions-N-18_19_null,NEXUS_5,1.0,en].png} | 0 ...ithReply-D-16_16_null,NEXUS_5,1.0,en].png} | 0 ...ithReply-N-16_17_null,NEXUS_5,1.0,en].png} | 0 ...nsLayout-D-19_19_null,NEXUS_5,1.0,en].png} | 0 ...nsLayout-N-19_20_null,NEXUS_5,1.0,en].png} | 0 ...ionsView-D-20_20_null,NEXUS_5,1.0,en].png} | 0 ...ionsView-N-20_21_null,NEXUS_5,1.0,en].png} | 0 ...sViewFew-D-21_21_null,NEXUS_5,1.0,en].png} | 0 ...sViewFew-N-21_22_null,NEXUS_5,1.0,en].png} | 0 ...Incoming-D-22_22_null,NEXUS_5,1.0,en].png} | 0 ...Incoming-N-22_23_null,NEXUS_5,1.0,en].png} | 0 ...Outgoing-D-23_23_null,NEXUS_5,1.0,en].png} | 0 ...Outgoing-N-23_24_null,NEXUS_5,1.0,en].png} | 0 ...EventRow-D-24_24_null,NEXUS_5,1.0,en].png} | 0 ...EventRow-N-24_25_null,NEXUS_5,1.0,en].png} | 0 ...InfoView-D-46_46_null,NEXUS_5,1.0,en].png} | 0 ...InfoView-N-46_47_null,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_0,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_1,NEXUS_5,1.0,en].png} | 0 ...ineView-D-7_7_null_10,NEXUS_5,1.0,en].png} | 0 ...ineView-D-7_7_null_11,NEXUS_5,1.0,en].png} | 0 ...ineView-D-7_7_null_12,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_2,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_3,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_4,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_5,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_6,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_7,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_8,NEXUS_5,1.0,en].png} | 0 ...lineView-D-7_7_null_9,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_0,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_1,NEXUS_5,1.0,en].png} | 0 ...ineView-N-7_8_null_10,NEXUS_5,1.0,en].png} | 0 ...ineView-N-7_8_null_11,NEXUS_5,1.0,en].png} | 0 ...ineView-N-7_8_null_12,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_2,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_3,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_4,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_5,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_6,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_7,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_8,NEXUS_5,1.0,en].png} | 0 ...lineView-N-7_8_null_9,NEXUS_5,1.0,en].png} | 0 ...ordButton-D-10_10_null,NEXUS_5,1.0,en].png | 3 + ...ordButton-N-10_11_null,NEXUS_5,1.0,en].png | 3 + ...gProgress-D-11_11_null,NEXUS_5,1.0,en].png | 3 + ...gProgress-N-11_12_null,NEXUS_5,1.0,en].png | 3 + ...ndButton-D-12_12_null,NEXUS_5,1.0,en].png} | 0 ...ndButton-N-12_13_null,NEXUS_5,1.0,en].png} | 0 ...rmatting-D-13_13_null,NEXUS_5,1.0,en].png} | 0 ...rmatting-N-13_14_null,NEXUS_5,1.0,en].png} | 0 ...mposerReply-D-3_3_null,NEXUS_5,1.0,en].png | 4 +- ...mposerReply-N-3_4_null,NEXUS_5,1.0,en].png | 4 +- ...poserSimple-D-0_0_null,NEXUS_5,1.0,en].png | 4 +- ...poserSimple-N-0_1_null,NEXUS_5,1.0,en].png | 4 +- 293 files changed, 967 insertions(+), 52 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt rename libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/{ => model}/Message.kt (92%) rename libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/{ => model}/MessageComposerMode.kt (97%) create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/PressEvent.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressState.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateEffects.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolder.kt create mode 100644 libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolderTest.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-24_24_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-25_25_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-24_25_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-25_26_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-26_26_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-27_27_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-26_27_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-27_28_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-29_29_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-30_30_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-29_30_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-30_31_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-30_30_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-30_30_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-30_31_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-30_31_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-31_31_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-31_31_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-31_32_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-31_32_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-33_33_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-34_34_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-33_34_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-34_35_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-34_34_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-35_35_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-34_35_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-35_36_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-36_36_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-37_37_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-36_37_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-37_38_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-37_37_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-37_37_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-37_37_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-37_38_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-37_38_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-37_38_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-38_38_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-39_39_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-38_39_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-39_40_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_16,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_16,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_17,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_17,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_18,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_18,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_19,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_19,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_20,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_20,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_21,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_21,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_16,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_16,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_17,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_17,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_18,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_18,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_19,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_19,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_20,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_20,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_21,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_21,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-40_40_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-41_41_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-40_41_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-41_42_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-41_41_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-42_42_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-41_41_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-42_42_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-41_42_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-42_43_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-41_42_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-42_43_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-42_42_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-43_43_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-42_43_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-43_44_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-43_43_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-44_44_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-43_43_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-44_44_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-43_44_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-44_45_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-43_44_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-44_45_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-44_44_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-45_45_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-44_45_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-45_46_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-8_8_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-9_9_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-8_9_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-9_10_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-10_10_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-11_11_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-10_11_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-11_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-9_9_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-9_9_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-9_9_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-9_9_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-11_11_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-12_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-11_12_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-12_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-12_12_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-13_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-12_13_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-13_14_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-13_13_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-13_13_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-13_13_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-13_13_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-13_14_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-13_14_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-13_14_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-13_14_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-14_14_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-15_15_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-14_15_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-15_16_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-16_16_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-16_16_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-16_16_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-16_16_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-16_17_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-16_17_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-16_17_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-16_17_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-17_17_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-18_18_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-17_18_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-18_19_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-15_15_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-16_16_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-15_16_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-16_17_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-18_18_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-19_19_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-18_19_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-19_20_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-19_19_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-20_20_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-19_20_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-20_21_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-20_20_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-21_21_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-20_21_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-21_22_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-21_21_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-22_22_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-21_22_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-22_23_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-22_22_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-23_23_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-22_23_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-23_24_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-23_23_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-24_24_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-23_24_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-24_25_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-45_45_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-46_46_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-45_46_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-46_47_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_9,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-10_10_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-10_11_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-D-11_11_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-N-11_12_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png} (100%) diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index 1bb99f48ce..b6f0dc8260 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -33,7 +33,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.SendLocationInvocation -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.delay diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt index 5a0596c7bb..d9a5e50e11 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt @@ -16,7 +16,7 @@ package io.element.android.features.messages.api -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode /** * Hoist-able state of the message composer. 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 20dc17eabc..8645553f0b 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 @@ -55,6 +55,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.store.PreferencesStore @@ -67,6 +68,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState +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.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -75,7 +78,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.matrix.ui.room.canRedactAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -84,6 +87,7 @@ import timber.log.Timber class MessagesPresenter @AssistedInject constructor( private val room: MatrixRoom, private val composerPresenter: MessageComposerPresenter, + private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, private val timelinePresenter: TimelinePresenter, private val actionListPresenter: ActionListPresenter, private val customReactionPresenter: CustomReactionPresenter, @@ -95,6 +99,7 @@ class MessagesPresenter @AssistedInject constructor( private val dispatchers: CoroutineDispatchers, private val clipboardHelper: ClipboardHelper, private val preferencesStore: PreferencesStore, + private val featureFlagsService: FeatureFlagService, @Assisted private val navigator: MessagesNavigator, ) : Presenter { @@ -107,6 +112,7 @@ class MessagesPresenter @AssistedInject constructor( override fun present(): MessagesState { val localCoroutineScope = rememberCoroutineScope() val composerState = composerPresenter.present() + val voiceMessageComposerState = voiceMessageComposerPresenter.present() val timelineState = timelinePresenter.present() val actionListState = actionListPresenter.present() val customReactionState = customReactionPresenter.present() @@ -145,6 +151,11 @@ class MessagesPresenter @AssistedInject constructor( val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) + var enableVoiceMessages by remember { mutableStateOf(false) } + LaunchedEffect(featureFlagsService) { + enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) + } + fun handleEvents(event: MessagesEvents) { when (event) { is MessagesEvents.HandleAction -> { @@ -177,6 +188,7 @@ class MessagesPresenter @AssistedInject constructor( userHasPermissionToSendMessage = userHasPermissionToSendMessage, userHasPermissionToRedact = userHasPermissionToRedact, composerState = composerState, + voiceMessageComposerState = voiceMessageComposerState, timelineState = timelineState, actionListState = actionListState, customReactionState = customReactionState, @@ -187,6 +199,7 @@ class MessagesPresenter @AssistedInject constructor( showReinvitePrompt = showReinvitePrompt, inviteProgress = inviteProgress.value, enableTextFormatting = enableTextFormatting, + enableVoiceMessages = enableVoiceMessages, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 31a66acd8c..0a121b50a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerState import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -36,6 +37,7 @@ data class MessagesState( val userHasPermissionToSendMessage: Boolean, val userHasPermissionToRedact: Boolean, val composerState: MessageComposerState, + val voiceMessageComposerState: VoiceMessageComposerState, val timelineState: TimelineState, val actionListState: ActionListState, val customReactionState: CustomReactionState, @@ -46,5 +48,6 @@ data class MessagesState( val inviteProgress: Async, val showReinvitePrompt: Boolean, val enableTextFormatting: Boolean, + val enableVoiceMessages: Boolean, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 5642bd4d2b..3b0b87ea39 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -25,11 +25,12 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.voicemessages.aVoiceMessageComposerState import io.element.android.libraries.architecture.Async 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 io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.persistentSetOf @@ -60,6 +61,7 @@ fun aMessagesState() = MessagesState( isFullScreen = false, mode = MessageComposerMode.Normal("Hello"), ), + voiceMessageComposerState = aVoiceMessageComposerState(), timelineState = aTimelineState().copy( timelineItems = aTimelineItemList(aTimelineItemTextContent()), ), @@ -82,5 +84,6 @@ fun aMessagesState() = MessagesState( inviteProgress = Async.Uninitialized, showReinvitePrompt = false, enableTextFormatting = true, + enableVoiceMessages = true, eventSink = {} ) 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 e112a451ad..971df06053 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 @@ -317,8 +317,10 @@ private fun MessagesViewContent( if (state.userHasPermissionToSendMessage) { MessageComposerView( state = state.composerState, + voiceMessageState = state.voiceMessageComposerState, subcomposing = subcomposing, enableTextFormatting = state.enableTextFormatting, + enableVoiceMessages = state.enableVoiceMessages, modifier = Modifier .fillMaxWidth(), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt index 73481cd617..c9a491d8a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt @@ -23,7 +23,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode import javax.inject.Inject @SingleIn(RoomScope::class) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index 92b180f326..cd03c52e1e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -17,8 +17,8 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable -import io.element.android.libraries.textcomposer.Message -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.Message +import io.element.android.libraries.textcomposer.model.MessageComposerMode @Immutable sealed interface MessageComposerEvents { 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 a45056ac67..e4a8d50cb8 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 @@ -47,8 +47,8 @@ import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter -import io.element.android.libraries.textcomposer.Message -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.Message +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.api.AnalyticsService import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.persistentListOf 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 ff74a81abe..3e8f171626 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 @@ -19,7 +19,7 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList 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 0483d945af..7dbe413e83 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 @@ -17,7 +17,7 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.wysiwyg.compose.RichTextEditorState open class MessageComposerStateProvider : PreviewParameterProvider { 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 938da1dcaf..150ae23f9b 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 @@ -24,17 +24,24 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerState +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerStateProvider +import io.element.android.features.messages.impl.voicemessages.aVoiceMessageComposerState import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.textcomposer.Message +import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.TextComposer +import io.element.android.libraries.textcomposer.model.PressEvent import kotlinx.coroutines.launch @Composable -fun MessageComposerView( +internal fun MessageComposerView( state: MessageComposerState, + voiceMessageState: VoiceMessageComposerState, subcomposing: Boolean, enableTextFormatting: Boolean, + enableVoiceMessages: Boolean, modifier: Modifier = Modifier, ) { fun sendMessage(message: Message) { @@ -64,9 +71,14 @@ fun MessageComposerView( } } + fun onVoiceRecordButtonEvent(press: PressEvent) { + voiceMessageState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(press)) + } + TextComposer( modifier = modifier, state = state.richTextEditorState, + voiceMessageState = voiceMessageState.voiceMessageState, subcomposing = subcomposing, onRequestFocus = ::onRequestFocus, onSendMessage = ::sendMessage, @@ -76,24 +88,49 @@ fun MessageComposerView( onAddAttachment = ::onAddAttachment, onDismissTextFormatting = ::onDismissTextFormatting, enableTextFormatting = enableTextFormatting, + enableVoiceMessages = enableVoiceMessages, + onVoiceRecordButtonEvent = ::onVoiceRecordButtonEvent, onError = ::onError, ) } @PreviewsDayNight @Composable -internal fun MessageComposerViewPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) = ElementPreview { +internal fun MessageComposerViewPreview( + @PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState, +) = ElementPreview { Column { MessageComposerView( modifier = Modifier.height(IntrinsicSize.Min), state = state, + voiceMessageState = aVoiceMessageComposerState(), enableTextFormatting = true, + enableVoiceMessages = true, subcomposing = false, ) MessageComposerView( modifier = Modifier.height(200.dp), state = state, + voiceMessageState = aVoiceMessageComposerState(), enableTextFormatting = true, + enableVoiceMessages = true, + subcomposing = false, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun MessageComposerViewVoicePreview( + @PreviewParameter(VoiceMessageComposerStateProvider::class) state: VoiceMessageComposerState, +) = ElementPreview { + Column { + MessageComposerView( + modifier = Modifier.height(IntrinsicSize.Min), + state = aMessageComposerState(), + voiceMessageState = state, + enableTextFormatting = true, + enableVoiceMessages = true, subcomposing = false, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt new file mode 100644 index 0000000000..72ec5ec635 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.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.features.messages.impl.voicemessages + +import io.element.android.libraries.textcomposer.model.PressEvent + +sealed class VoiceMessageComposerEvents { + data class RecordButtonEvent( + val pressEvent: PressEvent + ): VoiceMessageComposerEvents() +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt new file mode 100644 index 0000000000..106125934b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt @@ -0,0 +1,64 @@ +/* + * 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.voicemessages + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.VoiceMessageState +import javax.inject.Inject + +@SingleIn(RoomScope::class) +class VoiceMessageComposerPresenter @Inject constructor() : Presenter { + @Composable + override fun present(): VoiceMessageComposerState { + var voiceMessageState by remember { mutableStateOf(VoiceMessageState.Idle) } + + fun onRecordButtonPress(event: VoiceMessageComposerEvents.RecordButtonEvent) = when(event.pressEvent) { + PressEvent.PressStart -> { + // TODO start the recording + voiceMessageState = VoiceMessageState.Recording + } + PressEvent.LongPressEnd -> { + // TODO finish the recording + voiceMessageState = VoiceMessageState.Idle + } + PressEvent.Tapped -> { + // TODO discard the recording and show the 'hold to record' tooltip + voiceMessageState = VoiceMessageState.Idle + } + } + + + fun handleEvents(event: VoiceMessageComposerEvents) { + when (event) { + is VoiceMessageComposerEvents.RecordButtonEvent -> onRecordButtonPress(event) + } + } + + return VoiceMessageComposerState( + voiceMessageState = voiceMessageState, + eventSink = { handleEvents(it) } + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt new file mode 100644 index 0000000000..bacbe76324 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt @@ -0,0 +1,27 @@ +/* + * 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.voicemessages + +import androidx.compose.runtime.Stable +import io.element.android.libraries.textcomposer.model.VoiceMessageState + +@Stable +data class VoiceMessageComposerState( + val voiceMessageState: VoiceMessageState, + val eventSink: (VoiceMessageComposerEvents) -> Unit, +) + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt new file mode 100644 index 0000000000..63b59596c0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt @@ -0,0 +1,34 @@ +/* + * 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.voicemessages + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.textcomposer.model.VoiceMessageState + +internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording), + ) +} + +internal fun aVoiceMessageComposerState( + voiceMessageState: VoiceMessageState = VoiceMessageState.Idle, +) = VoiceMessageComposerState( + voiceMessageState = voiceMessageState, + eventSink = {}, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index def74c108a..b02afd90fb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider @@ -73,7 +74,7 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate @@ -619,6 +620,7 @@ class MessagesPresenterTest { richTextEditorStateFactory = TestRichTextEditorStateFactory(), permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), ) + val voiceMessageComposerPresenter = VoiceMessageComposerPresenter() val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, @@ -634,6 +636,7 @@ class MessagesPresenterTest { return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, + voiceMessageComposerPresenter = voiceMessageComposerPresenter, timelinePresenter = timelinePresenter, actionListPresenter = actionListPresenter, customReactionPresenter = customReactionPresenter, @@ -645,6 +648,7 @@ class MessagesPresenterTest { navigator = navigator, clipboardHelper = clipboardHelper, preferencesStore = preferencesStore, + featureFlagsService = FakeFeatureFlagService(), dispatchers = coroutineDispatchers, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 3f86ed8b80..ee951920a7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -58,8 +58,8 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory -import io.element.android.libraries.textcomposer.Message -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.Message +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.waitForPredicate diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt new file mode 100644 index 0000000000..008226bf05 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt @@ -0,0 +1,89 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.voicemessages + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter +import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.VoiceMessageState +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class VoiceMessageComposerPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + } + } + + @Test + fun `present - recording state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + + assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Recording) + } + } + + @Test + fun `present - abort recording`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.Tapped)) + + assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + } + } + + @Test + fun `present - finish recording`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + + assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + } + } + private fun createPresenter() = VoiceMessageComposerPresenter() +} diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt index 75c992f495..0ae604004d 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt @@ -17,7 +17,7 @@ package io.element.android.features.messages.test import io.element.android.features.messages.api.MessageComposerContext -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode class MessageComposerContextFake( override var composerMode: MessageComposerMode = MessageComposerMode.Normal(null) diff --git a/libraries/textcomposer/impl/build.gradle.kts b/libraries/textcomposer/impl/build.gradle.kts index 1b96fec0e6..bdc5e2b3c5 100644 --- a/libraries/textcomposer/impl/build.gradle.kts +++ b/libraries/textcomposer/impl/build.gradle.kts @@ -37,4 +37,8 @@ dependencies { api(libs.matrix.richtexteditor.compose) ksp(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.coroutines.test) } 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 75b94cdfb9..18246f1ac4 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 @@ -37,6 +37,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,8 +47,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleUp import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text @@ -61,9 +63,15 @@ import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.textcomposer.components.ComposerOptionsButton import io.element.android.libraries.textcomposer.components.DismissTextFormattingButton +import io.element.android.libraries.textcomposer.components.RecordButton +import io.element.android.libraries.textcomposer.components.RecordingProgress import io.element.android.libraries.textcomposer.components.SendButton import io.element.android.libraries.textcomposer.components.TextFormatting import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape +import io.element.android.libraries.textcomposer.model.Message +import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.compose.RichTextEditor @@ -74,8 +82,10 @@ import kotlinx.collections.immutable.persistentListOf @Composable fun TextComposer( state: RichTextEditorState, + voiceMessageState: VoiceMessageState, composerMode: MessageComposerMode, enableTextFormatting: Boolean, + enableVoiceMessages: Boolean, modifier: Modifier = Modifier, showTextFormatting: Boolean = false, subcomposing: Boolean = false, @@ -84,6 +94,7 @@ fun TextComposer( onResetComposerMode: () -> Unit = {}, onAddAttachment: () -> Unit = {}, onDismissTextFormatting: () -> Unit = {}, + onVoiceRecordButtonEvent: (PressEvent) -> Unit = {}, onError: (Throwable) -> Unit = {}, ) { val onSendClicked = { @@ -118,16 +129,34 @@ fun TextComposer( ) } + val canSendMessage by remember { derivedStateOf { state.messageHtml.isNotEmpty() } } val sendButton = @Composable { SendButton( - canSendMessage = state.messageHtml.isNotEmpty(), + canSendMessage = canSendMessage, onClick = onSendClicked, composerMode = composerMode, ) } + val recordButton = @Composable { + RecordButton( + onPressStart = { onVoiceRecordButtonEvent(PressEvent.PressStart) }, + onLongPressEnd = { onVoiceRecordButtonEvent(PressEvent.LongPressEnd) }, + onTap = { onVoiceRecordButtonEvent(PressEvent.Tapped) }, + ) + } val textFormattingOptions = @Composable { TextFormatting(state = state) } + val sendOrRecordButton = if (canSendMessage || !enableVoiceMessages) { + sendButton + } else { + recordButton + } + + val recordingProgress = @Composable { + RecordingProgress() + } + if (showTextFormatting) { TextFormattingLayout( modifier = layoutModifier, @@ -136,14 +165,16 @@ fun TextComposer( DismissTextFormattingButton(onClick = onDismissTextFormatting) }, textFormatting = textFormattingOptions, - sendButton = sendButton + sendButton = sendButton, ) } else { StandardLayout( + voiceMessageState = voiceMessageState, modifier = layoutModifier, composerOptionsButton = composerOptionsButton, textInput = textInput, - sendButton = sendButton + endButton = sendOrRecordButton, + recordingProgress = recordingProgress, ) } @@ -158,33 +189,45 @@ fun TextComposer( @Composable private fun StandardLayout( + voiceMessageState: VoiceMessageState, textInput: @Composable () -> Unit, composerOptionsButton: @Composable () -> Unit, - sendButton: @Composable () -> Unit, + recordingProgress: @Composable () -> Unit, + endButton: @Composable () -> Unit, modifier: Modifier = Modifier, ) { Row( modifier = modifier, verticalAlignment = Alignment.Bottom, ) { - Box( - Modifier - .padding(bottom = 5.dp, top = 5.dp, start = 3.dp) - ) { - composerOptionsButton() - } - Box( - modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) - ) { - textInput() + if (voiceMessageState is VoiceMessageState.Recording) { + Box( + modifier = Modifier + .padding(start = 16.dp, bottom = 8.dp, top = 8.dp) + .weight(1f) + ) { + recordingProgress() + } + } else { + Box( + Modifier + .padding(bottom = 5.dp, top = 5.dp, start = 3.dp) + ) { + composerOptionsButton() + } + Box( + modifier = Modifier + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) + ) { + textInput() + } } Box( Modifier .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) ) { - sendButton() + endButton() } } } @@ -438,18 +481,22 @@ internal fun TextComposerSimplePreview() = ElementPreview { { TextComposer( RichTextEditorState("", initialFocus = true), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState("A message", initialFocus = true), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( @@ -457,18 +504,22 @@ internal fun TextComposerSimplePreview() = ElementPreview { "A message\nWith several lines\nTo preview larger textfields and long lines with overflow", initialFocus = true ), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState("A message without focus", initialFocus = false), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }) ) @@ -480,23 +531,29 @@ internal fun TextComposerFormattingPreview() = ElementPreview { PreviewColumn(items = persistentListOf({ TextComposer( RichTextEditorState("", initialFocus = false), + voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, composerMode = MessageComposerMode.Normal(""), enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState("A message", initialFocus = false), + voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, composerMode = MessageComposerMode.Normal(""), enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", initialFocus = false), + voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, composerMode = MessageComposerMode.Normal(""), enableTextFormatting = true, + enableVoiceMessages = true, ) })) } @@ -507,10 +564,12 @@ internal fun TextComposerEditPreview() = ElementPreview { PreviewColumn(items = persistentListOf({ TextComposer( RichTextEditorState("A message", initialFocus = true), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) })) } @@ -521,6 +580,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { PreviewColumn(items = persistentListOf({ TextComposer( RichTextEditorState(""), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -533,11 +593,13 @@ internal fun TextComposerReplyPreview() = ElementPreview { ), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState(""), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = true, @@ -550,10 +612,12 @@ internal fun TextComposerReplyPreview() = ElementPreview { ), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState("A message"), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = true, @@ -569,10 +633,12 @@ internal fun TextComposerReplyPreview() = ElementPreview { ), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState("A message"), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -588,10 +654,12 @@ internal fun TextComposerReplyPreview() = ElementPreview { ), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState("A message"), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -607,10 +675,12 @@ internal fun TextComposerReplyPreview() = ElementPreview { ), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }, { TextComposer( RichTextEditorState("A message", initialFocus = true), + voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -626,6 +696,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { ), onResetComposerMode = {}, enableTextFormatting = true, + enableVoiceMessages = true, ) }) ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt new file mode 100644 index 0000000000..7c70dd1ef6 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt @@ -0,0 +1,108 @@ +/* + * 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.textcomposer.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.utils.PressState +import io.element.android.libraries.textcomposer.utils.PressStateEffects +import io.element.android.libraries.textcomposer.utils.rememberPressState +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.launch + +@Composable +internal fun RecordButton( + modifier: Modifier = Modifier, + onPressStart: () -> Unit = {}, + onLongPressEnd: () -> Unit = {}, + onTap: () -> Unit = {}, +) { + val coroutineScope = rememberCoroutineScope() + val pressState = rememberPressState() + + PressStateEffects( + pressState = pressState.value, + onPressStart = onPressStart, + onLongPressEnd = onLongPressEnd, + onTap = onTap, + ) + + RecordButtonView( + isPressed = pressState.value is PressState.Pressing, + modifier = modifier + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + coroutineScope.launch { + when (event.type) { + PointerEventType.Press -> pressState.press() + PointerEventType.Release -> pressState.release() + } + } + } + } + } + ) +} + +@Composable +private fun RecordButtonView( + isPressed: Boolean, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .size(48.dp), + onClick = {}, + ) { + Icon( + modifier = Modifier.size(24.dp.applyScaleUp()), + resourceId = if (isPressed) { + CommonDrawables.ic_compound_mic_on_solid + } else { + CommonDrawables.ic_compound_mic_on_outline + }, + contentDescription = stringResource(CommonStrings.a11y_voice_message_record), + tint = ElementTheme.colors.iconSecondary, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun RecordButtonPreview() = ElementPreview { + Row { + RecordButtonView(isPressed = false) + RecordButtonView(isPressed = true) + } +} + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt new file mode 100644 index 0000000000..db4f59342c --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt @@ -0,0 +1,74 @@ +/* + * 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.textcomposer.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme + +@Composable +internal fun RecordingProgress( + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = ElementTheme.colors.bgSubtleSecondary, + shape = MaterialTheme.shapes.medium, + ) + .padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) + .heightIn(26.dp) + + , + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) + ) + Spacer(Modifier.size(8.dp)) + + // TODO Replace with timer UI + Text( + text = "Recording...", // Not localized because it is a placeholder + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodySmMedium + ) + } +} + +@PreviewsDayNight +@Composable +internal fun RecordingProgressPreview() { + RecordingProgress() +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt index 5ac16bfd14..8dc1a4706b 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -36,7 +36,7 @@ import io.element.android.libraries.designsystem.text.applyScaleUp import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt index cf975634f6..81f64e6ebe 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt @@ -22,7 +22,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.text.applyScaleUp -import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.libraries.textcomposer.model.MessageComposerMode @Composable internal fun textInputRoundedCornerShape( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Message.kt similarity index 92% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Message.kt index ebc066188a..226adc5e57 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Message.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.textcomposer +package io.element.android.libraries.textcomposer.model data class Message( val html: String?, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt similarity index 97% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index 3dbc652aaf..49ce0ddb6e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.textcomposer +package io.element.android.libraries.textcomposer.model import android.os.Parcelable import io.element.android.libraries.matrix.api.core.EventId diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/PressEvent.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/PressEvent.kt new file mode 100644 index 0000000000..96dff11cad --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/PressEvent.kt @@ -0,0 +1,23 @@ +/* + * 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.textcomposer.model + +sealed class PressEvent { + data object PressStart: PressEvent() + data object Tapped: PressEvent() + data object LongPressEnd: PressEvent() +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt new file mode 100644 index 0000000000..d376c4ee70 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -0,0 +1,22 @@ +/* + * 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.textcomposer.model + +sealed class VoiceMessageState { + data object Idle: VoiceMessageState() + data object Recording: VoiceMessageState() +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressState.kt new file mode 100644 index 0000000000..714581feb1 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressState.kt @@ -0,0 +1,31 @@ +/* + * 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.textcomposer.utils + +/** + * State of a press gesture. + */ +internal sealed class PressState { + data class Idle( + val lastPress: Pressing? + ) : PressState() + + sealed class Pressing : PressState() + data object Tapping : Pressing() + data object LongPressing : Pressing() +} + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateEffects.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateEffects.kt new file mode 100644 index 0000000000..aaee6bae0f --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateEffects.kt @@ -0,0 +1,47 @@ +/* + * 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.textcomposer.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect + +/** + * React to [PressState] changes. + */ +@Composable +internal fun PressStateEffects( + pressState: PressState, + onPressStart: () -> Unit = {}, + onLongPressStart: () -> Unit = {}, + onTap: () -> Unit = {}, + onLongPressEnd: () -> Unit = {}, +) { + LaunchedEffect(pressState) { + when (pressState) { + is PressState.Idle -> + when (pressState.lastPress) { + PressState.Tapping -> onTap() + PressState.LongPressing -> onLongPressEnd() + null -> {} // Do nothing + } + is PressState.LongPressing -> onLongPressStart() + PressState.Tapping -> onPressStart() + } + } +} + + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolder.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolder.kt new file mode 100644 index 0000000000..7021b8ac46 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolder.kt @@ -0,0 +1,101 @@ +/* + * 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.textcomposer.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalViewConfiguration +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.yield +import timber.log.Timber + +@Composable +internal fun rememberPressState( + longPressTimeoutMillis: Long = LocalViewConfiguration.current.longPressTimeoutMillis, +): PressStateHolder { + return remember(longPressTimeoutMillis) { + PressStateHolder(longPressTimeoutMillis = longPressTimeoutMillis) + } +} + +/** + * State machine that keeps track of the pressed state. + * + * When a press is started, the state will transition through: + * [PressState.Idle] -> [PressState.Tapping] -> ... + * + * If a press is held for a longer time, the state will continue through: + * ... -> [PressState.LongPressing] -> ... + * + * When the press is released the states will then transition back to idle. + * ... -> [PressState.Idle] + * + * Whether a press should be considered a tap or a long press can be determined by + * looking at the last press when in the idle state. + * + * @see [PressStateEffects] + * @see [rememberPressState] + */ +internal class PressStateHolder( + private val longPressTimeoutMillis: Long, +) : State { + private var state: PressState by mutableStateOf(PressState.Idle(lastPress = null)) + + override val value: PressState + get() = state + + private var longPressTimer: Job? = null + + suspend fun press() = coroutineScope { + when (state) { + is PressState.Idle -> { + state = PressState.Tapping + } + is PressState.Pressing -> + Timber.e("Pointer pressed but it has not been released") + } + + longPressTimer = launch { + delay(longPressTimeoutMillis) + yield() + + if (isActive && state == PressState.Tapping) { + state = PressState.LongPressing + } + } + } + + fun release() { + longPressTimer?.cancel() + longPressTimer = null + when (val lastState = state) { + is PressState.Pressing -> + state = PressState.Idle(lastPress = lastState) + is PressState.Idle -> + Timber.e("Pointer pressed but it has not been released") + } + } +} + diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolderTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolderTest.kt new file mode 100644 index 0000000000..615692911e --- /dev/null +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolderTest.kt @@ -0,0 +1,111 @@ +/* + * 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.textcomposer.utils + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.textcomposer.utils.PressState.Idle +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds + +@OptIn(ExperimentalCoroutinesApi::class) class PressStateHolderTest { + companion object { + const val LONG_PRESS_TIMEOUT_MILLIS = 1L + } + @Test + fun `it starts in idle state`() = runTest { + val stateHolder = createStateHolder() + assertThat(stateHolder.value).isEqualTo(Idle(lastPress = null)) + } + + @Test + fun `when press, it moves to tapping state`() = runTest { + val stateHolder = createStateHolder() + val press = async { stateHolder.press() } + advanceTimeBy(1.milliseconds) + assertThat(stateHolder.value).isEqualTo(PressState.Tapping) + press.await() + } + + @Test + fun `when release after short delay, it moves through tap states`() = runTest { + val stateHolder = createStateHolder() + val press = async { stateHolder.press() } + advanceTimeBy(1.milliseconds) + assertThat(stateHolder.value).isEqualTo(PressState.Tapping) + stateHolder.release() + advanceTimeBy(1.milliseconds) // wait for the long press timeout which should not be triggered + assertThat(stateHolder.value).isEqualTo(Idle(lastPress = PressState.Tapping)) + press.await() + } + + @Test + fun `when hold, it moves through long press states`() = runTest { + val stateHolder = createStateHolder() + val press = async { stateHolder.press() } + advanceTimeBy(1.milliseconds) + assertThat(stateHolder.value).isEqualTo(PressState.Tapping) + advanceTimeBy(1.milliseconds) + assertThat(stateHolder.value).isEqualTo(PressState.LongPressing) + stateHolder.release() + assertThat(stateHolder.value).isEqualTo(Idle(lastPress = PressState.LongPressing)) + press.await() + } + + @Test + fun `when release and repress, it doesn't enter long press states`() = runTest { + val stateHolder = createStateHolder() + val press1 = async { stateHolder.press() } + advanceTimeBy(1.milliseconds) + assertThat(stateHolder.value).isEqualTo(PressState.Tapping) + stateHolder.release() + val press2 = async { stateHolder.press() } + advanceTimeBy(1.milliseconds) + assertThat(stateHolder.value).isEqualTo(PressState.Tapping) + press1.await() + press2.await() + } + + @Test + fun `when press twice without releasing, it doesn't throw an error`() = runTest { + val stateHolder = createStateHolder() + stateHolder.press() + stateHolder.press() + } + + @Test + fun `when release without first pressing, it doesn't throw an error`() = runTest { + val stateHolder = createStateHolder() + stateHolder.release() + } + + @Test + fun `when release twice without pressing, it doesn't throw an error `() = runTest { + val stateHolder = createStateHolder() + stateHolder.press() + stateHolder.release() + stateHolder.release() + } + + private fun createStateHolder() = + PressStateHolder( + LONG_PRESS_TIMEOUT_MILLIS, + ) +} diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 6a7f0ecbb7..539c215a07 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -1,13 +1,17 @@ + "Delete" "Hide password" "Mentions only" "Muted" + "Pause" + "Play" "Poll" "Ended poll" "Send files" "Show password" "User menu" + "Record voice message. Double tap and hold to record. Release to end recording." "Accept" "Add to timeline" "Back" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png index 9c2309187b..eb1900be3a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b91884f47f7789ccb1e22b29be6cfcfa6fa5975614145eedd4d884b867cfcf0f -size 18458 +oid sha256:7b8e9c99cb13a27ebc80c664d185cd3e3ad7b2bf6a7a944022c662ba1ee59ab7 +size 18876 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png index e119b62da1..d1e45b4cb1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c0892fa19589139b4d47a514f0a56486ab47f82b94ef9426f8bb623be7cc0cb -size 16786 +oid sha256:6e26cc86e44601b64ed04e62e2e714252896d669c98d5aebb0b0df1d852b8ab6 +size 17261 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8a7c62e651 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96999517a90c46cc256a1703eb0919adc81682478344e8dab922f39ebb000530 +size 8420 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..482fd62f5a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7be984402697ade0648a7c7cf6785f6f22ac3f3fbb44bc3e1a71be73400461f +size 8057 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-5_5_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-5_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-5_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-5_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-5_6_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-5_6_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-5_6_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-24_24_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-25_25_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.customreaction_null_EmojiPicker-D-24_24_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-25_25_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-24_25_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-25_26_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.customreaction_null_EmojiPicker-N-24_25_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-25_26_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_0,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.event_null_TimelineItemAudioView-D-25_25_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_1,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.event_null_TimelineItemAudioView-D-25_25_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-25_25_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_2,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.event_null_TimelineItemAudioView-D-25_25_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-26_26_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_0,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.event_null_TimelineItemAudioView-N-25_26_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_1,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.event_null_TimelineItemAudioView-N-25_26_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-25_26_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_2,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.event_null_TimelineItemAudioView-N-25_26_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-26_27_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-26_26_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-27_27_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.event_null_TimelineItemEncryptedView-D-26_26_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-27_27_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-26_27_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-27_28_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.event_null_TimelineItemEncryptedView-N-26_27_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-27_28_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_0,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.event_null_TimelineItemFileView-D-27_27_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_1,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.event_null_TimelineItemFileView-D-27_27_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-27_27_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_2,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.event_null_TimelineItemFileView-D-27_27_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-28_28_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_0,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.event_null_TimelineItemFileView-N-27_28_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_1,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.event_null_TimelineItemFileView-N-27_28_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-27_28_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_2,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.event_null_TimelineItemFileView-N-27_28_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-28_29_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_0,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.event_null_TimelineItemImageView-D-28_28_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_1,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.event_null_TimelineItemImageView-D-28_28_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-28_28_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_2,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.event_null_TimelineItemImageView-D-28_28_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-29_29_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_0,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.event_null_TimelineItemImageView-N-28_29_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_1,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.event_null_TimelineItemImageView-N-28_29_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-28_29_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_2,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.event_null_TimelineItemImageView-N-28_29_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-29_30_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-29_29_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-30_30_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.event_null_TimelineItemInformativeView-D-29_29_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-30_30_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-29_30_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-30_31_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.event_null_TimelineItemInformativeView-N-29_30_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-30_31_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-30_30_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_0,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.event_null_TimelineItemLocationView-D-30_30_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-30_30_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_1,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.event_null_TimelineItemLocationView-D-30_30_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-31_31_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-30_31_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_0,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.event_null_TimelineItemLocationView-N-30_31_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-30_31_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_1,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.event_null_TimelineItemLocationView-N-30_31_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-31_32_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_0,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.event_null_TimelineItemPollCreatorView-D-32_32_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-32_32_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_1,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.event_null_TimelineItemPollCreatorView-D-32_32_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-33_33_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_0,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.event_null_TimelineItemPollCreatorView-N-32_33_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-32_33_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_1,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.event_null_TimelineItemPollCreatorView-N-32_33_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-33_34_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-31_31_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_0,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.event_null_TimelineItemPollView-D-31_31_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-31_31_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_1,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.event_null_TimelineItemPollView-D-31_31_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-32_32_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-31_32_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_0,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.event_null_TimelineItemPollView-N-31_32_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-31_32_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_1,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.event_null_TimelineItemPollView-N-31_32_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-32_33_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-33_33_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-34_34_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.event_null_TimelineItemRedactedView-D-33_33_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-34_34_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-33_34_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-34_35_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.event_null_TimelineItemRedactedView-N-33_34_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-34_35_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-34_34_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-35_35_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.event_null_TimelineItemStateView-D-34_34_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-35_35_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-34_35_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-35_36_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.event_null_TimelineItemStateView-N-34_35_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-35_36_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_0,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.event_null_TimelineItemTextView-D-35_35_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_1,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.event_null_TimelineItemTextView-D-35_35_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_2,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.event_null_TimelineItemTextView-D-35_35_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_3,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.event_null_TimelineItemTextView-D-35_35_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_4,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.event_null_TimelineItemTextView-D-35_35_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-35_35_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_5,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.event_null_TimelineItemTextView-D-35_35_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-36_36_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_0,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.event_null_TimelineItemTextView-N-35_36_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_1,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.event_null_TimelineItemTextView-N-35_36_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_2,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.event_null_TimelineItemTextView-N-35_36_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_3,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.event_null_TimelineItemTextView-N-35_36_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_4,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.event_null_TimelineItemTextView-N-35_36_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-35_36_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_5,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.event_null_TimelineItemTextView-N-35_36_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-36_37_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-36_36_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-37_37_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.event_null_TimelineItemUnknownView-D-36_36_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-37_37_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-36_37_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-37_38_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.event_null_TimelineItemUnknownView-N-36_37_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-37_38_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-37_37_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_0,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.event_null_TimelineItemVideoView-D-37_37_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-37_37_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_1,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.event_null_TimelineItemVideoView-D-37_37_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-37_37_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_2,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.event_null_TimelineItemVideoView-D-37_37_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-38_38_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-37_38_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_0,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.event_null_TimelineItemVideoView-N-37_38_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-37_38_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_1,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.event_null_TimelineItemVideoView-N-37_38_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-37_38_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_2,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.event_null_TimelineItemVideoView-N-37_38_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-38_39_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-38_38_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-39_39_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.group_null_GroupHeaderView-D-38_38_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-39_39_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-38_39_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-39_40_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.group_null_GroupHeaderView-N-38_39_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-39_40_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_0,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.html_null_HtmlDocument-D-39_39_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_1,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.html_null_HtmlDocument-D-39_39_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_10,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.html_null_HtmlDocument-D-39_39_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_11,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.html_null_HtmlDocument-D-39_39_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_12,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.html_null_HtmlDocument-D-39_39_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_13,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.html_null_HtmlDocument-D-39_39_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_14,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.html_null_HtmlDocument-D-39_39_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_15,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.html_null_HtmlDocument-D-39_39_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_16,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.html_null_HtmlDocument-D-39_39_null_16,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_16,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_17,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.html_null_HtmlDocument-D-39_39_null_17,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_17,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_18,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.html_null_HtmlDocument-D-39_39_null_18,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_18,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_19,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.html_null_HtmlDocument-D-39_39_null_19,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_19,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_2,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.html_null_HtmlDocument-D-39_39_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_20,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.html_null_HtmlDocument-D-39_39_null_20,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_20,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_21,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.html_null_HtmlDocument-D-39_39_null_21,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_21,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_3,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.html_null_HtmlDocument-D-39_39_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_4,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.html_null_HtmlDocument-D-39_39_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_5,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.html_null_HtmlDocument-D-39_39_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_6,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.html_null_HtmlDocument-D-39_39_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_7,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.html_null_HtmlDocument-D-39_39_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_8,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.html_null_HtmlDocument-D-39_39_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-39_39_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_9,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.html_null_HtmlDocument-D-39_39_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-40_40_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_0,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.html_null_HtmlDocument-N-39_40_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_1,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.html_null_HtmlDocument-N-39_40_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_10,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.html_null_HtmlDocument-N-39_40_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_11,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.html_null_HtmlDocument-N-39_40_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_12,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.html_null_HtmlDocument-N-39_40_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_13,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.html_null_HtmlDocument-N-39_40_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_14,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.html_null_HtmlDocument-N-39_40_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_15,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.html_null_HtmlDocument-N-39_40_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_16,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.html_null_HtmlDocument-N-39_40_null_16,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_16,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_17,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.html_null_HtmlDocument-N-39_40_null_17,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_17,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_18,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.html_null_HtmlDocument-N-39_40_null_18,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_18,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_19,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.html_null_HtmlDocument-N-39_40_null_19,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_19,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_2,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.html_null_HtmlDocument-N-39_40_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_20,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.html_null_HtmlDocument-N-39_40_null_20,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_20,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_21,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.html_null_HtmlDocument-N-39_40_null_21,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_21,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_3,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.html_null_HtmlDocument-N-39_40_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_4,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.html_null_HtmlDocument-N-39_40_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_5,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.html_null_HtmlDocument-N-39_40_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_6,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.html_null_HtmlDocument-N-39_40_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_7,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.html_null_HtmlDocument-N-39_40_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_8,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.html_null_HtmlDocument-N-39_40_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-39_40_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_9,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.html_null_HtmlDocument-N-39_40_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-40_41_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-40_40_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-41_41_null_0,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.reactionsummary_null_SheetContent-D-40_40_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-41_41_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-40_41_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-41_42_null_0,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.reactionsummary_null_SheetContent-N-40_41_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-41_42_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-41_41_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-42_42_null_0,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.retrysendmenu_null_RetrySendMessageMenu-D-41_41_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-42_42_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-41_41_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-42_42_null_1,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.retrysendmenu_null_RetrySendMessageMenu-D-41_41_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-42_42_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-41_42_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-42_43_null_0,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.retrysendmenu_null_RetrySendMessageMenu-N-41_42_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-42_43_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-41_42_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-42_43_null_1,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.retrysendmenu_null_RetrySendMessageMenu-N-41_42_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-42_43_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-42_42_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-43_43_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_null_TimelineEncryptedHistoryBannerView-D-42_42_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-43_43_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_null_TimelineEncryptedHistoryBannerView-N-42_43_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-43_44_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_null_TimelineEncryptedHistoryBannerView-N-42_43_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-43_44_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_null_TimelineItemDaySeparatorView-D-43_43_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-44_44_null_0,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_null_TimelineItemDaySeparatorView-D-43_43_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-44_44_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-43_43_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-44_44_null_1,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_null_TimelineItemDaySeparatorView-D-43_43_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_null_TimelineItemDaySeparatorView-D-44_44_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-43_44_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-44_45_null_0,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_null_TimelineItemDaySeparatorView-N-43_44_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-44_45_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-43_44_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-44_45_null_1,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_null_TimelineItemDaySeparatorView-N-43_44_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_null_TimelineItemDaySeparatorView-N-44_45_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-44_44_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-45_45_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_null_TimelineLoadingMoreIndicator-D-44_44_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-45_45_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_null_TimelineLoadingMoreIndicator-N-44_45_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-45_46_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_null_TimelineLoadingMoreIndicator-N-44_45_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-45_46_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_0,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_null_MessageEventBubble-D-7_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_1,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_null_MessageEventBubble-D-7_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_10,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_null_MessageEventBubble-D-7_7_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_11,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_null_MessageEventBubble-D-7_7_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_12,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_null_MessageEventBubble-D-7_7_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_13,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_null_MessageEventBubble-D-7_7_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_14,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_null_MessageEventBubble-D-7_7_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_15,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_null_MessageEventBubble-D-7_7_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_2,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_null_MessageEventBubble-D-7_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_3,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_null_MessageEventBubble-D-7_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_4,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_null_MessageEventBubble-D-7_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_5,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_null_MessageEventBubble-D-7_7_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_6,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_null_MessageEventBubble-D-7_7_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_7,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_null_MessageEventBubble-D-7_7_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_8,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_null_MessageEventBubble-D-7_7_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-7_7_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_9,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_null_MessageEventBubble-D-7_7_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_10,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_null_MessageEventBubble-N-7_8_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_11,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_null_MessageEventBubble-N-7_8_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_12,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_null_MessageEventBubble-N-7_8_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_13,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_null_MessageEventBubble-N-7_8_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_14,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_null_MessageEventBubble-N-7_8_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_15,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_null_MessageEventBubble-N-7_8_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_5,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_null_MessageEventBubble-N-7_8_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_6,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_null_MessageEventBubble-N-7_8_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_7,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_null_MessageEventBubble-N-7_8_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_8,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_null_MessageEventBubble-N-7_8_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-7_8_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_9,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_null_MessageEventBubble-N-7_8_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-8_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-9_9_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_null_MessageStateEventContainer-D-8_8_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-9_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-9_10_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_null_MessageStateEventContainer-N-8_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-9_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-10_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-11_11_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_null_MessagesAddReactionButton-D-10_10_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-11_11_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-11_12_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_null_MessagesAddReactionButton-N-10_11_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-11_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-9_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_0,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_null_MessagesReactionButton-D-9_9_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-9_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_1,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_null_MessagesReactionButton-D-9_9_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-9_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_2,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_null_MessagesReactionButton-D-9_9_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-9_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_3,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_null_MessagesReactionButton-D-9_9_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-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.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-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.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_2,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_null_MessagesReactionButton-N-9_10_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-9_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_3,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_null_MessagesReactionButton-N-9_10_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-12_12_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_null_MessagesReactionExtraButtons-D-11_11_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-12_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-12_13_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_null_MessagesReactionExtraButtons-N-11_12_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-12_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-12_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-13_13_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_null_ReplySwipeIndicator-D-12_12_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-13_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-12_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-13_14_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_null_ReplySwipeIndicator-N-12_13_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-13_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-13_13_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_0,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_null_TimelineEventTimestampView-D-13_13_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-13_13_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_1,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_null_TimelineEventTimestampView-D-13_13_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-13_13_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_2,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_null_TimelineEventTimestampView-D-13_13_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-13_13_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_3,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_null_TimelineEventTimestampView-D-13_13_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-13_14_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_0,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_null_TimelineEventTimestampView-N-13_14_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-13_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_1,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_null_TimelineEventTimestampView-N-13_14_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-13_14_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_2,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_null_TimelineEventTimestampView-N-13_14_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-13_14_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_3,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_null_TimelineEventTimestampView-N-13_14_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-14_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-15_15_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_null_TimelineItemEventRow-D-14_14_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-15_15_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-14_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-15_16_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_null_TimelineItemEventRow-N-14_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-15_16_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-16_16_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_0,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_null_TimelineItemEventRowTimestamp-D-16_16_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-16_16_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_1,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_null_TimelineItemEventRowTimestamp-D-16_16_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-16_16_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_2,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_null_TimelineItemEventRowTimestamp-D-16_16_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-16_16_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_3,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_null_TimelineItemEventRowTimestamp-D-16_16_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-16_17_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_0,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_null_TimelineItemEventRowTimestamp-N-16_17_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-16_17_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_1,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_null_TimelineItemEventRowTimestamp-N-16_17_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-16_17_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_2,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_null_TimelineItemEventRowTimestamp-N-16_17_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-16_17_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_3,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_null_TimelineItemEventRowTimestamp-N-16_17_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-17_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-18_18_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_null_TimelineItemEventRowWithManyReactions-D-17_17_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-18_18_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-17_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-18_19_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_null_TimelineItemEventRowWithManyReactions-N-17_18_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-18_19_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-16_16_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_null_TimelineItemEventRowWithReply-D-15_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-16_16_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-16_17_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_null_TimelineItemEventRowWithReply-N-15_16_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-16_17_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-18_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-19_19_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_null_TimelineItemReactionsLayout-D-18_18_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-19_19_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-18_19_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-19_20_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_null_TimelineItemReactionsLayout-N-18_19_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-19_20_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-19_19_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-20_20_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_null_TimelineItemReactionsView-D-19_19_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-20_20_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-19_20_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-20_21_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_null_TimelineItemReactionsView-N-19_20_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-20_21_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-20_20_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-21_21_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_null_TimelineItemReactionsViewFew-D-20_20_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-21_21_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-20_21_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-21_22_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_null_TimelineItemReactionsViewFew-N-20_21_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-21_22_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-21_21_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-22_22_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_null_TimelineItemReactionsViewIncoming-D-21_21_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-22_22_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-21_22_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-22_23_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_null_TimelineItemReactionsViewIncoming-N-21_22_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-22_23_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-22_22_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-23_23_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_null_TimelineItemReactionsViewOutgoing-D-22_22_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-23_23_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-22_23_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-23_24_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_null_TimelineItemReactionsViewOutgoing-N-22_23_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-23_24_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-23_23_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-24_24_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_null_TimelineItemStateEventRow-D-23_23_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-24_24_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-23_24_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-24_25_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_null_TimelineItemStateEventRow-N-23_24_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-24_25_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-45_45_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-46_46_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.debug_null_EventDebugInfoView-D-45_45_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-46_46_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-45_46_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-46_47_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.debug_null_EventDebugInfoView-N-45_46_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-46_47_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_12,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-6_6_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-6_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-6_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_12,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-6_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-6_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-6_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-6_7_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-6_7_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-6_7_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-6_7_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-10_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-10_10_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f0c42b1810 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-10_10_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0e8a1efaf22dd86e6e27904d72a820cfb0b5d1d38ffeb5745bfa6ca3b0a1c85 +size 6003 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-10_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..27368109ad --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-10_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f2db2983459eae8b7538c51d125836807b29334b01fbca495c3c9630fb510c2 +size 5969 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-D-11_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..015499b3dc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-D-11_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47e727f1ff283a022aa174266b83c837aa60b379c0c410e94d9ff6b792f2aead +size 7390 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-N-11_12_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8c5cc08564 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-N-11_12_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16ac898bc3ca7c827f52612fb5f2c6051de4f746a1328dacaf6fba1cefa6f896 +size 7053 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-10_10_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-10_11_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-11_11_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-11_12_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png index d47caa7de0..9c64fdc104 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe441727d1a64a7439a15780a208b50bfacd875d6a6fa9bee980508e9beda5a7 -size 87262 +oid sha256:339df847ee60d9b2b6da7458b9b99c54877551325cfcdcefe8677f4f9edd72e3 +size 87578 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png index a42ece91ed..b0073ea6e0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:031e8632fb034425c6e30801abb5f57271e5411784501ee27cd413db363e2d67 -size 83036 +oid sha256:ff51194566d9c09bb054cf7be73147215ec267382ce7bf0552f4b8de6bb357ec +size 83398 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png index a6f891a3a0..f00037d6dd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd38c36b9b85c3ca3e290e2c0fd338ca52552398d2497a92a573ffd3f133b567 -size 47937 +oid sha256:f50efca595312474668341a2bb6715992286bb52d4645774ed9fb1a83f94ca9f +size 48112 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png index c3d1dc9ae0..9d14e51ab1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:033292295440c19363ebf1cc425c60226cf6f811a25219ae7939c8d2871091c4 -size 45020 +oid sha256:7c50736a0b06dd848b79b263cdb231fa9b3ae23ad049615d6073ce1838b8ca5f +size 45219 From 0ad7e8fab0b040de46e1892991f36039024dbe03 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 12 Oct 2023 17:45:22 +0200 Subject: [PATCH 026/281] Pin setup : exclude pin presenters from test coverage check --- build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index f08c023b1d..fcd67f07fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -250,6 +250,8 @@ koverMerged { excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" + // Temporary until we have actually something to test. + excludes += "io.element.android.features.pin.impl.*Presenter" } bound { minValue = 85 From c0aa70f7f60b37e18621d58edfd94b9d966ae945 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 18:41:38 +0200 Subject: [PATCH 027/281] Move konsist test to their own module. --- app/build.gradle.kts | 1 - settings.gradle.kts | 1 + tests/konsist/build.gradle.kts | 34 ++++++++++++++ .../tests/konsist}/KonsistArchitectureTest.kt | 2 +- .../tests/konsist}/KonsistClassNameTest.kt | 2 +- .../tests/konsist}/KonsistComposableTest.kt | 2 +- .../tests/konsist/KonsistConfigTest.kt | 45 +++++++++++++++++++ .../tests/konsist}/KonsistFieldTest.kt | 2 +- .../tests/konsist}/KonsistPreviewTest.kt | 2 +- .../android/tests/konsist}/KonsistTestTest.kt | 2 +- 10 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 tests/konsist/build.gradle.kts rename {app/src/test/kotlin/io/element/android/app => tests/konsist/src/test/kotlin/io/element/android/tests/konsist}/KonsistArchitectureTest.kt (98%) rename {app/src/test/kotlin/io/element/android/app => tests/konsist/src/test/kotlin/io/element/android/tests/konsist}/KonsistClassNameTest.kt (98%) rename {app/src/test/kotlin/io/element/android/app => tests/konsist/src/test/kotlin/io/element/android/tests/konsist}/KonsistComposableTest.kt (98%) create mode 100644 tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistConfigTest.kt rename {app/src/test/kotlin/io/element/android/app => tests/konsist/src/test/kotlin/io/element/android/tests/konsist}/KonsistFieldTest.kt (96%) rename {app/src/test/kotlin/io/element/android/app => tests/konsist/src/test/kotlin/io/element/android/tests/konsist}/KonsistPreviewTest.kt (97%) rename {app/src/test/kotlin/io/element/android/app => tests/konsist/src/test/kotlin/io/element/android/tests/konsist}/KonsistTestTest.kt (97%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5286bde045..839a5095dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -230,7 +230,6 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) - testImplementation(libs.test.konsist) ksp(libs.showkase.processor) } diff --git a/settings.gradle.kts b/settings.gradle.kts index e1894c16d7..105befcd04 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,6 +52,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "ElementX" include(":app") include(":appnav") +include(":tests:konsist") include(":tests:uitests") include(":tests:testutils") include(":anvilannotations") diff --git a/tests/konsist/build.gradle.kts b/tests/konsist/build.gradle.kts new file mode 100644 index 0000000000..ca009bd89f --- /dev/null +++ b/tests/konsist/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * 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") +} + +android { + namespace = "io.element.android.tests.konsist" +} + +dependencies { + val composeBom = platform(libs.androidx.compose.bom) + testImplementation(composeBom) + testImplementation("androidx.compose.ui:ui-tooling-preview") + testImplementation(libs.test.junit) + testImplementation(libs.test.konsist) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.architecture) + testImplementation(projects.libraries.designsystem) +} diff --git a/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt similarity index 98% rename from app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt rename to tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt index 17e9710653..f4bd7175fd 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistArchitectureTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.app +package io.element.android.tests.konsist import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.constructors diff --git a/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt similarity index 98% rename from app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt rename to tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index d187816682..43e82b5dc2 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.app +package io.element.android.tests.konsist import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.bumble.appyx.core.node.Node diff --git a/app/src/test/kotlin/io/element/android/app/KonsistComposableTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistComposableTest.kt similarity index 98% rename from app/src/test/kotlin/io/element/android/app/KonsistComposableTest.kt rename to tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistComposableTest.kt index c09f3c07a8..9eb23cfc61 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistComposableTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistComposableTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.app +package io.element.android.tests.konsist import androidx.compose.runtime.Composable import com.lemonappdev.konsist.api.KoModifier diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistConfigTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistConfigTest.kt new file mode 100644 index 0000000000..45247adc60 --- /dev/null +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistConfigTest.kt @@ -0,0 +1,45 @@ +/* + * 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.tests.konsist + +import com.google.common.truth.Truth.assertThat +import com.lemonappdev.konsist.api.Konsist +import org.junit.Test + +class KonsistConfigTest { + @Test + fun `assert that Konsist detect all the project classes`() { + assertThat( + Konsist + .scopeFromProject() + .classes() + .size + ) + .isGreaterThan(1_000) + } + + @Test + fun `assert that Konsist detect all the test classes`() { + assertThat( + Konsist + .scopeFromTest() + .classes() + .size + ) + .isGreaterThan(100) + } +} diff --git a/app/src/test/kotlin/io/element/android/app/KonsistFieldTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistFieldTest.kt similarity index 96% rename from app/src/test/kotlin/io/element/android/app/KonsistFieldTest.kt rename to tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistFieldTest.kt index 13d5c9949b..867c1ad141 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistFieldTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistFieldTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.app +package io.element.android.tests.konsist import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.properties diff --git a/app/src/test/kotlin/io/element/android/app/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt similarity index 97% rename from app/src/test/kotlin/io/element/android/app/KonsistPreviewTest.kt rename to tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index f9bcf620d0..40c73c8eaa 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.app +package io.element.android.tests.konsist import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.withAllAnnotationsOf diff --git a/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt similarity index 97% rename from app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt rename to tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt index 3ed6bd2026..32fd6dd624 100644 --- a/app/src/test/kotlin/io/element/android/app/KonsistTestTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.app +package io.element.android.tests.konsist import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.ext.list.modifierprovider.withoutOverrideModifier From 5e9b0b1165cb8ba98dd176aac8dc89a02774b4dc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 18:44:33 +0200 Subject: [PATCH 028/281] Fix warning Name contains characters which can cause problems on Windows: " --- .../io/element/android/tests/konsist/KonsistClassNameTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index 43e82b5dc2..64ca2e899b 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -46,7 +46,7 @@ class KonsistClassNameTest { } @Test - fun `Classes extending 'PreviewParameterProvider' name MUST end with "Provider" and MUST contain provided class name`() { + fun `Classes extending 'PreviewParameterProvider' name MUST end with 'Provider' and MUST contain provided class name`() { Konsist.scopeFromProject() .classes() .withAllParentsOf(PreviewParameterProvider::class) From 3be6e655d7d17d21cfa500c59f5c37a4cd2cec5c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Oct 2023 18:46:14 +0200 Subject: [PATCH 029/281] Fix issue detected by Konsist. --- .../messages/impl/voicemessages/VoiceMessageComposerEvents.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt index 72ec5ec635..7d6803fc41 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt @@ -18,8 +18,8 @@ package io.element.android.features.messages.impl.voicemessages import io.element.android.libraries.textcomposer.model.PressEvent -sealed class VoiceMessageComposerEvents { +sealed interface VoiceMessageComposerEvents { data class RecordButtonEvent( val pressEvent: PressEvent - ): VoiceMessageComposerEvents() + ): VoiceMessageComposerEvents } From 35d59982afd675019cd2961ad5b0361920881516 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:07:57 +0000 Subject: [PATCH 030/281] Update dependency io.sentry:sentry-android to v6.31.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f9ae6ddc3..3588e9f230 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -162,7 +162,7 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" # Analytics posthog = "com.posthog.android:posthog:2.0.3" -sentry = "io.sentry:sentry-android:6.30.0" +sentry = "io.sentry:sentry-android:6.31.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f" # Emojibase From 054d0a7145297c22024466453995a31ccc16671f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 08:19:30 +0000 Subject: [PATCH 031/281] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.62 (#1562) * Update dependency org.matrix.rustcomponents:sdk-android to v0.1.62 * Add `sendVoiceMessage()` API from https://github.com/matrix-org/matrix-rust-sdk/pull/2697 * Fix other breaking changes --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Marco Romano --- gradle/libs.versions.toml | 2 +- .../libraries/matrix/api/room/MatrixRoom.kt | 7 +++++++ .../libraries/matrix/impl/RustMatrixClient.kt | 6 ++---- .../matrix/impl/room/RustMatrixRoom.kt | 17 +++++++++++++++-- .../matrix/test/room/FakeMatrixRoom.kt | 7 +++++++ 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f9ae6ddc3..de9ee16942 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -143,7 +143,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.61" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.62" 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 = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } 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 e14138f837..746f8cead8 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 @@ -185,5 +185,12 @@ interface MatrixRoom : Closeable { */ suspend fun endPoll(pollStartId: EventId, text: String): Result + suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback? + ): Result + override fun close() = destroy() } 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 8b857d8076..f557a0fc0f 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 @@ -298,10 +298,9 @@ class RustMatrixClient constructor( runCatching { client.setDisplayName(displayName) } } - @OptIn(ExperimentalUnsignedTypes::class) override suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result = withContext(sessionDispatcher) { - runCatching { client.uploadAvatar(mimeType, data.toUByteArray().toList()) } + runCatching { client.uploadAvatar(mimeType, data) } } override suspend fun removeAvatar(): Result = @@ -382,10 +381,9 @@ class RustMatrixClient constructor( } } - @OptIn(ExperimentalUnsignedTypes::class) override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result = withContext(sessionDispatcher) { runCatching { - client.uploadMedia(mimeType, data.toUByteArray().toList(), progressCallback?.toProgressWatcher()) + client.uploadMedia(mimeType, data, progressCallback?.toProgressWatcher()) } } 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 2fd9760b21..87cc742549 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 @@ -373,10 +373,9 @@ class RustMatrixRoom( } } - @OptIn(ExperimentalUnsignedTypes::class) override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = withContext(roomDispatcher) { runCatching { - innerRoom.uploadAvatar(mimeType, data.toUByteArray().toList(), null) + innerRoom.uploadAvatar(mimeType, data, null) } } @@ -465,6 +464,20 @@ class RustMatrixRoom( } } + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback?, + ): Result = sendAttachment(listOf(file)) { + innerRoom.sendVoiceMessage( + url = file.path, + audioInfo = audioInfo.map(), + waveform = waveform.map { it.toUShort() }, + progressWatcher = progressCallback?.toProgressWatcher(), + ) + } + private suspend fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) 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 4934867b55..7549522c0c 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 @@ -361,6 +361,13 @@ class FakeMatrixRoom( return endPollResult } + override suspend fun sendVoiceMessage( + file: File, + audioInfo: AudioInfo, + waveform: List, + progressCallback: ProgressCallback? + ): Result = fakeSendMedia(progressCallback) + fun givenLeaveRoomError(throwable: Throwable?) { this.leaveRoomError = throwable } From 76bbe4dbebaf45d225bf77f98ff0397403bc77d4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 13 Oct 2023 11:01:03 +0200 Subject: [PATCH 032/281] Misc: remove usage of blocking methods #1563 --- changelog.d/1563.misc | 1 + .../libraries/matrix/impl/RustMatrixClient.kt | 5 +-- .../matrix/impl/RustMatrixClientFactory.kt | 3 +- .../RustNotificationSettingsService.kt | 28 +++++++-------- .../matrix/impl/room/RustMatrixRoom.kt | 26 +++++++------- .../impl/roomlist/RoomSummaryListProcessor.kt | 8 ++--- .../matrix/impl/sync/RustSyncService.kt | 27 ++++++-------- .../impl/timeline/RoomTimelineExtensions.kt | 2 +- .../impl/timeline/RustMatrixTimeline.kt | 2 +- .../RustSessionVerificationService.kt | 35 ++++++------------- 10 files changed, 58 insertions(+), 79 deletions(-) create mode 100644 changelog.d/1563.misc diff --git a/changelog.d/1563.misc b/changelog.d/1563.misc new file mode 100644 index 0000000000..8de1c9b6d1 --- /dev/null +++ b/changelog.d/1563.misc @@ -0,0 +1 @@ +Remove usage of blocking methods. 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 f557a0fc0f..14cf3cd17e 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 @@ -98,8 +98,8 @@ class RustMatrixClient constructor( private val innerRoomListService = syncService.roomListService() private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}") - private val rustSyncService = RustSyncService(syncService, dispatchers, sessionCoroutineScope) - private val verificationService = RustSessionVerificationService(rustSyncService, dispatchers) + private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope) + private val verificationService = RustSessionVerificationService(rustSyncService) private val pushersService = RustPushersService( client = client, dispatchers = dispatchers, @@ -208,6 +208,7 @@ class RustMatrixClient constructor( private fun pairOfRoom(roomId: RoomId): Pair? { val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value) + // Keep using fullRoomBlocking for now as it's faster. val fullRoom = cachedRoomListItem?.fullRoomBlocking() return if (cachedRoomListItem == null || fullRoom == null) { Timber.d("No room cached for $roomId") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index bddf904533..b37266342e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -53,7 +53,8 @@ class RustMatrixClientFactory @Inject constructor( client.restoreSession(sessionData.toSession()) - val syncService = client.syncService().finishBlocking() + val syncService = client.syncService() + .finish() RustMatrixClient( client = client, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt index 618c12cc03..a2fffdbdfb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -47,19 +47,15 @@ class RustNotificationSettingsService( notificationSettings.setDelegate(notificationSettingsDelegate) } - override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result = withContext( - dispatchers.io - ) { + override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result = runCatching { - notificationSettings.getRoomNotificationSettingsBlocking(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map) + notificationSettings.getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map) } - } - override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result = withContext(dispatchers.io) { + override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result = runCatching { - notificationSettings.getDefaultRoomNotificationModeBlocking(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) + notificationSettings.getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) } - } override suspend fun setDefaultRoomNotificationMode( isEncrypted: Boolean, @@ -67,19 +63,19 @@ class RustNotificationSettingsService( isOneToOne: Boolean ): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.setDefaultRoomNotificationModeBlocking(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) + notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) } } override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.setRoomNotificationModeBlocking(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) + notificationSettings.setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) } } override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.restoreDefaultRoomNotificationModeBlocking(roomId.value) + notificationSettings.restoreDefaultRoomNotificationMode(roomId.value) } } @@ -87,31 +83,31 @@ class RustNotificationSettingsService( override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean) = withContext(dispatchers.io) { runCatching { - notificationSettings.unmuteRoomBlocking(roomId.value, isEncrypted, isOneToOne) + notificationSettings.unmuteRoom(roomId.value, isEncrypted, isOneToOne) } } override suspend fun isRoomMentionEnabled(): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.isRoomMentionEnabledBlocking() + notificationSettings.isRoomMentionEnabled() } } override suspend fun setRoomMentionEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.setRoomMentionEnabledBlocking(enabled) + notificationSettings.setRoomMentionEnabled(enabled) } } override suspend fun isCallEnabled(): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.isCallEnabledBlocking() + notificationSettings.isCallEnabled() } } override suspend fun setCallEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.setCallEnabledBlocking(enabled) + notificationSettings.setCallEnabled(enabled) } } } 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 87cc742549..4dcdb6d88c 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 @@ -174,7 +174,7 @@ class RustMatrixRoom( _membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers) var rustMembers: List? = null try { - rustMembers = innerRoom.membersBlocking().use { membersIterator -> + rustMembers = innerRoom.members().use { membersIterator -> buildList { while (true) { // Loading the whole membersIterator as a stop-gap measure. @@ -299,27 +299,27 @@ class RustMatrixRoom( } } - override suspend fun canUserInvite(userId: UserId): Result = withContext(roomMembersDispatcher) { - runCatching { - innerRoom.canUserInviteBlocking(userId.value) + override suspend fun canUserInvite(userId: UserId): Result { + return runCatching { + innerRoom.canUserInvite(userId.value) } } - override suspend fun canUserRedact(userId: UserId): Result = withContext(roomMembersDispatcher) { - runCatching { - innerRoom.canUserRedactBlocking(userId.value) + override suspend fun canUserRedact(userId: UserId): Result { + return runCatching { + innerRoom.canUserRedact(userId.value) } } - override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result = withContext(roomMembersDispatcher) { - runCatching { - innerRoom.canUserSendStateBlocking(userId.value, type.map()) + override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result { + return runCatching { + innerRoom.canUserSendState(userId.value, type.map()) } } - override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result = withContext(roomMembersDispatcher) { - runCatching { - innerRoom.canUserSendMessageBlocking(userId.value, type.map()) + override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result { + return runCatching { + innerRoom.canUserSendMessage(userId.value, type.map()) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt index 754e35ec66..d0e3d1c8cf 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessor.kt @@ -73,7 +73,7 @@ class RoomSummaryListProcessor( } } - private fun MutableList.applyUpdate(update: RoomListEntriesUpdate) { + private suspend fun MutableList.applyUpdate(update: RoomListEntriesUpdate) { when (update) { is RoomListEntriesUpdate.Append -> { val roomSummaries = update.values.map { @@ -119,7 +119,7 @@ class RoomSummaryListProcessor( } } - private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary { + private suspend fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary { return when (entry) { RoomListEntry.Empty -> buildEmptyRoomSummary() is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId) @@ -133,9 +133,9 @@ class RoomSummaryListProcessor( return RoomSummary.Empty(UUID.randomUUID().toString()) } - private fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary { + private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary { val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem -> - roomListItem.roomInfoBlocking().use { roomInfo -> + roomListItem.roomInfo().use { roomInfo -> RoomSummary.Filled( details = roomSummaryDetailsFactory.create(roomInfo) ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt index 259c2330b9..932da42afb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.matrix.impl.sync -import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.CoroutineScope @@ -26,33 +25,27 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.SyncServiceInterface import org.matrix.rustcomponents.sdk.SyncServiceState import timber.log.Timber class RustSyncService( private val innerSyncService: SyncServiceInterface, - private val dispatchers: CoroutineDispatchers, sessionCoroutineScope: CoroutineScope ) : SyncService { - override suspend fun startSync() = withContext(dispatchers.io) { - runCatching { - Timber.i("Start sync") - innerSyncService.startBlocking() - }.onFailure { - Timber.d("Start sync failed: $it") - } + override suspend fun startSync() = runCatching { + Timber.i("Start sync") + innerSyncService.start() + }.onFailure { + Timber.d("Start sync failed: $it") } - override suspend fun stopSync() = withContext(dispatchers.io){ - runCatching { - Timber.i("Stop sync") - innerSyncService.stopBlocking() - }.onFailure { - Timber.d("Stop sync failed: $it") - } + override suspend fun stopSync() = runCatching { + Timber.i("Stop sync") + innerSyncService.stop() + }.onFailure { + Timber.d("Stop sync failed: $it") } override val syncState: StateFlow = diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt index 7b38ba6823..bddd2bc872 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt @@ -44,7 +44,7 @@ internal fun Room.timelineDiffFlow(onInitialList: suspend (List) - } val roomId = id() Timber.d("Open timelineDiffFlow for room $roomId") - val result = addTimelineListenerBlocking(listener) + val result = addTimelineListener(listener) try { onInitialList(result.items) } catch (exception: Exception) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index e453019b52..1f0d13c9bf 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -124,7 +124,7 @@ class RustMatrixTimeline( private suspend fun fetchMembers() = withContext(dispatcher) { initLatch.await() - innerRoom.fetchMembersBlocking() + innerRoom.fetchMembers() } @OptIn(ExperimentalCoroutinesApi::class) 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 01646b1e4b..29797ed3c4 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 @@ -16,7 +16,6 @@ package io.element.android.libraries.matrix.impl.verification -import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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.SessionVerificationService @@ -28,7 +27,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.SessionVerificationController import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface @@ -37,7 +35,6 @@ import javax.inject.Inject class RustSessionVerificationService @Inject constructor( private val syncService: RustSyncService, - private val dispatchers: CoroutineDispatchers, ) : SessionVerificationService, SessionVerificationControllerDelegate { var verificationController: SessionVerificationControllerInterface? = null @@ -64,31 +61,21 @@ class RustSessionVerificationService @Inject constructor( syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified } - override suspend fun requestVerification() { - tryOrFail { - verificationController?.requestVerificationBlocking() - } + override suspend fun requestVerification() = tryOrFail { + verificationController?.requestVerification() } - override suspend fun cancelVerification() { - tryOrFail { verificationController?.cancelVerificationBlocking() } + override suspend fun cancelVerification() = tryOrFail { verificationController?.cancelVerification() } + + override suspend fun approveVerification() = tryOrFail { verificationController?.approveVerification() } + + override suspend fun declineVerification() = tryOrFail { verificationController?.declineVerification() } + + override suspend fun startVerification() = tryOrFail { + verificationController?.startSasVerification() } - override suspend fun approveVerification() { - tryOrFail { verificationController?.approveVerificationBlocking() } - } - - override suspend fun declineVerification() { - tryOrFail { verificationController?.declineVerificationBlocking() } - } - - override suspend fun startVerification() { - tryOrFail { - verificationController?.startSasVerificationBlocking() - } - } - - private suspend fun tryOrFail(block: suspend () -> Unit) = withContext(dispatchers.io) { + private suspend fun tryOrFail(block: suspend () -> Unit) { runCatching { block() }.onFailure { didFail() } From 2d3d007e60acf9088657af220b3e5966db20bbaf Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 13 Oct 2023 14:55:12 +0200 Subject: [PATCH 033/281] Find and replace --- gradle/libs.versions.toml | 10 +++++----- .../encrypteddb/SqlCipherDriverFactory.kt | 4 ++-- .../session-storage/impl/build.gradle.kts | 18 ++++++++++-------- .../impl/DatabaseSessionStore.kt | 6 +++--- .../impl/DatabaseSessionStoreTests.kt | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43e24a9e1b..7b4032a4a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ appyx = "1.4.0" dependencycheck = "8.4.0" dependencyanalysis = "1.25.0" stem = "2.3.0" -sqldelight = "1.5.5" +sqldelight = "2.0.0" telephoto = "0.6.2" wysiwyg = "2.14.1" @@ -146,9 +146,9 @@ timber = "com.jakewharton.timber:timber:5.0.1" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.62" 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 = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } -sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } -sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } +sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } +sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4" sqlite = "androidx.sqlite:sqlite-ktx:2.3.1" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" @@ -203,7 +203,7 @@ dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencyc dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" } paparazzi = "app.cash.paparazzi:1.3.1" kover = "org.jetbrains.kotlinx.kover:0.6.1" -sqldelight = { id = "com.squareup.sqldelight", version.ref = "sqldelight" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } # Version '4.3.1.3277' introduced some regressions in CI time (more than 2x slower), so make sure # this is no longer the case before upgrading. diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt index 5258c388c5..5de674d1ca 100644 --- a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt @@ -17,8 +17,8 @@ package io.element.encrypteddb import android.content.Context -import com.squareup.sqldelight.android.AndroidSqliteDriver -import com.squareup.sqldelight.db.SqlDriver +import app.cash.sqldelight.android.AndroidSqliteDriver +import app.cash.sqldelight.db.SqlDriver import io.element.encrypteddb.passphrase.PassphraseProvider import net.sqlcipher.database.SupportFactory diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 4e76c7de9c..03de9acf86 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -50,13 +50,15 @@ dependencies { } sqldelight { - database("SessionDatabase") { - // https://cashapp.github.io/sqldelight/1.5.4/multiplatform_sqlite/migrations/ - // To generate a .db file from your latest schema, run this task - // ./gradlew generateDebugSessionDatabaseSchema - // Test migration by running - // ./gradlew verifySqlDelightMigration - schemaOutputDirectory = File("src/main/sqldelight/databases") - verifyMigrations = true + databases { + create("SessionDatabase") { + // https://cashapp.github.io/sqldelight/2.0.0/android_sqlite/migrations/ + // To generate a .db file from your latest schema, run this task + // ./gradlew generateDebugSessionDatabaseSchema + // Test migration by running + // ./gradlew verifySqlDelightMigration + schemaOutputDirectory = File("src/main/sqldelight/databases") + verifyMigrations = true + } } } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index 0ca63d52a7..d799a39dac 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -17,9 +17,9 @@ package io.element.android.libraries.sessionstorage.impl import com.squareup.anvil.annotations.ContributesBinding -import com.squareup.sqldelight.runtime.coroutines.asFlow -import com.squareup.sqldelight.runtime.coroutines.mapToList -import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull +import app.cash.sqldelight.runtime.coroutines.asFlow +import app.cash.sqldelight.runtime.coroutines.mapToList +import app.cash.sqldelight.runtime.coroutines.mapToOneOrNull import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.sessionstorage.api.LoggedInState 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 4ca932f48f..30c586abe0 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 @@ -18,7 +18,7 @@ package io.element.android.libraries.sessionstorage.impl import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver +import app.cash.sqldelight.sqlite.driver.JdbcSqliteDriver import io.element.android.libraries.matrix.session.SessionData import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoginType From 57ccafca425ac08f91daa1236955b5552161be52 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 13 Oct 2023 15:17:12 +0200 Subject: [PATCH 034/281] Other fixes as per https://cashapp.github.io/sqldelight/2.0.0/upgrading-2.0/ --- .../element/encrypteddb/SqlCipherDriverFactory.kt | 6 ++++-- .../sessionstorage/impl/DatabaseSessionStore.kt | 12 +++++++----- .../impl/DatabaseSessionStoreTests.kt | 15 +++++++++++++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt index 5de674d1ca..cabc41b4ed 100644 --- a/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt +++ b/libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt @@ -17,8 +17,10 @@ package io.element.encrypteddb import android.content.Context -import app.cash.sqldelight.android.AndroidSqliteDriver +import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.db.SqlSchema +import app.cash.sqldelight.driver.android.AndroidSqliteDriver import io.element.encrypteddb.passphrase.PassphraseProvider import net.sqlcipher.database.SupportFactory @@ -35,7 +37,7 @@ class SqlCipherDriverFactory( * @param name The name of the database to create. * @param context Android [Context], used to instantiate the driver. */ - fun create(schema: SqlDriver.Schema, name: String, context: Context): SqlDriver { + fun create(schema: SqlSchema>, name: String, context: Context): SqlDriver { val passphrase = passphraseProvider.getPassphrase() val factory = SupportFactory(passphrase) return AndroidSqliteDriver(schema = schema, context = context, name = name, factory = factory) diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index d799a39dac..c437a4ef08 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -16,10 +16,11 @@ package io.element.android.libraries.sessionstorage.impl +import app.cash.sqldelight.coroutines.asFlow +import app.cash.sqldelight.coroutines.mapToList +import app.cash.sqldelight.coroutines.mapToOneOrNull import com.squareup.anvil.annotations.ContributesBinding -import app.cash.sqldelight.runtime.coroutines.asFlow -import app.cash.sqldelight.runtime.coroutines.mapToList -import app.cash.sqldelight.runtime.coroutines.mapToOneOrNull +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.sessionstorage.api.LoggedInState @@ -34,12 +35,13 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DatabaseSessionStore @Inject constructor( private val database: SessionDatabase, + private val dispatchers: CoroutineDispatchers, ) : SessionStore { override fun isLoggedIn(): Flow { return database.sessionDataQueries.selectFirst() .asFlow() - .mapToOneOrNull() + .mapToOneOrNull(dispatchers.io) .map { if (it == null) { LoggedInState.NotLoggedIn @@ -96,7 +98,7 @@ class DatabaseSessionStore @Inject constructor( Timber.w("Observing session list!") return database.sessionDataQueries.selectAll() .asFlow() - .mapToList() + .mapToList(dispatchers.io) .map { it.map { sessionData -> sessionData.toApiModel() } } } 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 30c586abe0..cb5569fb62 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 @@ -16,12 +16,15 @@ package io.element.android.libraries.sessionstorage.impl +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import app.cash.sqldelight.sqlite.driver.JdbcSqliteDriver +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.session.SessionData import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoginType +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -44,6 +47,7 @@ class DatabaseSessionStoreTests { loginType = LoginType.UNKNOWN.name, ) + @OptIn(ExperimentalCoroutinesApi::class) @Before fun setup() { // Initialise in memory SQLite driver @@ -51,7 +55,14 @@ class DatabaseSessionStoreTests { SessionDatabase.Schema.create(driver) database = SessionDatabase(driver) - databaseSessionStore = DatabaseSessionStore(database) + databaseSessionStore = DatabaseSessionStore( + database = database, + dispatchers = CoroutineDispatchers( + io = UnconfinedTestDispatcher(), + computation = UnconfinedTestDispatcher(), + main = UnconfinedTestDispatcher(), + ) + ) } @Test From 3fc520160a605e40f5d96be955516196f71f8e26 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Oct 2023 15:44:06 +0200 Subject: [PATCH 035/281] Fix issue detected by Konsist. --- .../io/element/android/appnav/BackstackExt.kt | 21 ---------- .../MoveActivityToBackgroundBackHandler.kt | 39 +++++++++++++++++++ .../features/pin/impl/create/CreatePinView.kt | 2 +- ...ePinView-D-1_1_null_0,NEXUS_5,1.0,en].png} | 0 ...ePinView-N-1_2_null_0,NEXUS_5,1.0,en].png} | 0 5 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/MoveActivityToBackgroundBackHandler.kt rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.pin.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.pin.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png} (100%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt index e7bf9f200f..36b267debb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/BackstackExt.kt @@ -16,12 +16,6 @@ package io.element.android.appnav -import android.content.Context -import android.content.ContextWrapper -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.NewRoot import com.bumble.appyx.navmodel.backstack.operation.Remove @@ -47,18 +41,3 @@ fun BackStack.removeLast(element: T) { accept(Remove(lastExpectedNavElement.key)) } -@Composable -fun MoveActivityToBackgroundBackHandler(enabled: Boolean = true) { - - fun Context.findActivity(): ComponentActivity? = when (this) { - is ComponentActivity -> this - is ContextWrapper -> baseContext.findActivity() - else -> null - } - - val context = LocalContext.current - BackHandler(enabled = enabled) { - context.findActivity()?.moveTaskToBack(false) - } -} - diff --git a/appnav/src/main/kotlin/io/element/android/appnav/MoveActivityToBackgroundBackHandler.kt b/appnav/src/main/kotlin/io/element/android/appnav/MoveActivityToBackgroundBackHandler.kt new file mode 100644 index 0000000000..5d959f9464 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/MoveActivityToBackgroundBackHandler.kt @@ -0,0 +1,39 @@ +/* + * 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.appnav + +import android.content.Context +import android.content.ContextWrapper +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +@Composable +fun MoveActivityToBackgroundBackHandler(enabled: Boolean = true) { + + fun Context.findActivity(): ComponentActivity? = when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null + } + + val context = LocalContext.current + BackHandler(enabled = enabled) { + context.findActivity()?.moveTaskToBack(false) + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt index efdbe62bfa..64e5be4091 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt @@ -43,7 +43,7 @@ fun CreatePinView( @Composable @PreviewsDayNight -internal fun CreatePinViewLightPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { +internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { ElementPreview { CreatePinView( state = state, diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-D-1_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-N-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.pin.impl.create_null_CreatePinViewLight-N-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png From a3b25022cdf6bcb2b0134925d7184bc40ef6bb75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Oct 2023 17:22:05 +0200 Subject: [PATCH 036/281] Kover: properly exclude pin code presenters --- build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index fcd67f07fe..704bbe6bbc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -251,7 +251,9 @@ koverMerged { // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" // Temporary until we have actually something to test. - excludes += "io.element.android.features.pin.impl.*Presenter" + excludes += "io.element.android.features.pin.impl.auth.PinAuthenticationPresenter" + excludes += "io.element.android.features.pin.impl.auth.PinAuthenticationPresenter$*" + excludes += "io.element.android.features.pin.impl.create.CreatePinPresenter" } bound { minValue = 85 From 2789498abffe13451570ffc6f50b781d7e92d6fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:54:48 +0000 Subject: [PATCH 037/281] Update dependency com.google.testparameterinjector:test-parameter-injector to v1.13 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43e24a9e1b..c7e8fd02f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -127,7 +127,7 @@ test_hamcrest = "org.hamcrest:hamcrest:2.2" test_orchestrator = "androidx.test:orchestrator:1.4.2" test_turbine = "app.cash.turbine:turbine:1.0.0" test_truth = "com.google.truth:truth:1.1.5" -test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.12" +test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.13" test_robolectric = "org.robolectric:robolectric:4.10.3" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } From 586a53653df2780ac4ddf3ea2881c0310bfb0d6f Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 16 Oct 2023 00:09:47 +0000 Subject: [PATCH 038/281] Sync Strings from Localazy --- .../impl/src/main/res/values-sk/translations.xml | 2 +- .../impl/src/main/res/values-cs/translations.xml | 7 +------ .../impl/src/main/res/values-de/translations.xml | 6 ------ .../impl/src/main/res/values-es/translations.xml | 6 ------ .../impl/src/main/res/values-fr/translations.xml | 6 ------ .../impl/src/main/res/values-it/translations.xml | 6 ------ .../impl/src/main/res/values-ro/translations.xml | 6 ------ .../impl/src/main/res/values-ru/translations.xml | 7 +------ .../impl/src/main/res/values-sk/translations.xml | 7 +------ .../impl/src/main/res/values-zh-rTW/translations.xml | 6 ------ features/login/impl/src/main/res/values/localazy.xml | 7 +------ .../impl/src/main/res/values-cs/translations.xml | 2 +- .../impl/src/main/res/values-de/translations.xml | 1 - .../impl/src/main/res/values-es/translations.xml | 1 - .../impl/src/main/res/values-fr/translations.xml | 1 - .../impl/src/main/res/values-it/translations.xml | 1 - .../impl/src/main/res/values-ro/translations.xml | 1 - .../impl/src/main/res/values-ru/translations.xml | 1 - .../impl/src/main/res/values-sk/translations.xml | 2 +- .../impl/src/main/res/values-zh-rTW/translations.xml | 1 - .../messages/impl/src/main/res/values/localazy.xml | 2 +- .../impl/src/main/res/values-cs/translations.xml | 1 - .../impl/src/main/res/values-de/translations.xml | 1 - .../impl/src/main/res/values-fr/translations.xml | 1 - .../impl/src/main/res/values-ro/translations.xml | 1 - .../impl/src/main/res/values-ru/translations.xml | 1 - .../impl/src/main/res/values-sk/translations.xml | 1 - .../impl/src/main/res/values-zh-rTW/translations.xml | 1 - .../onboarding/impl/src/main/res/values/localazy.xml | 1 - .../impl/src/main/res/values-ru/translations.xml | 3 +++ .../api/src/main/res/values-cs/translations.xml | 1 - .../api/src/main/res/values-de/translations.xml | 1 - .../api/src/main/res/values-fr/translations.xml | 1 - .../api/src/main/res/values-ru/translations.xml | 2 +- .../api/src/main/res/values-sk/translations.xml | 2 +- .../impl/src/main/res/values-cs/translations.xml | 1 - .../impl/src/main/res/values-de/translations.xml | 1 - .../impl/src/main/res/values-es/translations.xml | 1 - .../impl/src/main/res/values-fr/translations.xml | 1 - .../impl/src/main/res/values-it/translations.xml | 1 - .../impl/src/main/res/values-ro/translations.xml | 1 - .../impl/src/main/res/values-ru/translations.xml | 1 - .../impl/src/main/res/values-sk/translations.xml | 1 - .../impl/src/main/res/values-zh-rTW/translations.xml | 1 - .../impl/src/main/res/values/localazy.xml | 1 - .../impl/src/main/res/values-cs/translations.xml | 8 ++++++++ .../impl/src/main/res/values-ru/translations.xml | 8 ++++++++ .../impl/src/main/res/values-sk/translations.xml | 8 ++++++++ .../impl/src/main/res/values-cs/translations.xml | 2 -- .../impl/src/main/res/values-de/translations.xml | 2 -- .../impl/src/main/res/values-es/translations.xml | 2 -- .../impl/src/main/res/values-fr/translations.xml | 2 -- .../impl/src/main/res/values-it/translations.xml | 2 -- .../impl/src/main/res/values-ro/translations.xml | 2 -- .../impl/src/main/res/values-ru/translations.xml | 2 -- .../impl/src/main/res/values-sk/translations.xml | 2 -- .../impl/src/main/res/values-zh-rTW/translations.xml | 2 -- .../impl/src/main/res/values/localazy.xml | 2 -- .../src/main/res/values-cs/translations.xml | 10 ++++++---- .../src/main/res/values-de/translations.xml | 4 ---- .../src/main/res/values-es/translations.xml | 3 --- .../src/main/res/values-fr/translations.xml | 4 ---- .../src/main/res/values-it/translations.xml | 3 --- .../src/main/res/values-ro/translations.xml | 4 ---- .../src/main/res/values-ru/translations.xml | 11 +++++++---- .../src/main/res/values-sk/translations.xml | 12 +++++++----- .../src/main/res/values-zh-rTW/translations.xml | 6 +++--- .../ui-strings/src/main/res/values/localazy.xml | 5 +---- 68 files changed, 61 insertions(+), 152 deletions(-) create mode 100644 features/signedout/impl/src/main/res/values-cs/translations.xml create mode 100644 features/signedout/impl/src/main/res/values-ru/translations.xml create mode 100644 features/signedout/impl/src/main/res/values-sk/translations.xml diff --git a/features/ftue/impl/src/main/res/values-sk/translations.xml b/features/ftue/impl/src/main/res/values-sk/translations.xml index aa76053cea..c7eb1f83be 100644 --- a/features/ftue/impl/src/main/res/values-sk/translations.xml +++ b/features/ftue/impl/src/main/res/values-sk/translations.xml @@ -8,6 +8,6 @@ "História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii." "Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení." "Poďme na to!" - "Tu je to, čo potrebujete vedieť:" + "Toto by ste mali vedieť:" "Vitajte v %1$s!" diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index 5d697bf34a..3edf7d1003 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -18,6 +18,7 @@ "Adresa URL domovského serveru" "Můžete se připojit pouze k serveru, který podporuje klouzavou synchronizaci. Správce vašeho domovského serveru jej bude muset nakonfigurovat. %1$s" "Jaká je adresa vašeho serveru?" + "Vyberte váš server" "Tento účet byl deaktivován." "Nesprávné uživatelské jméno nebo heslo" "Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'" @@ -37,11 +38,5 @@ Díky za trpělivost!" "Vítá vás %1$s" "Jste v pořadníku!" "Jdete do toho!" - "Pokračovat" - "Pokračovat" - "Vyberte svůj server" - "Heslo" - "Pokračovat" "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." - "Uživatelské jméno" diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index a7511c0f09..ac7df075db 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -37,11 +37,5 @@ Danke für deine Geduld!" "Willkommen bei %1$s!" "Du bist fast am Ziel." "Du bist dabei." - "Weiter" - "Weiter" - "Wähle deinen Server aus" - "Passwort" - "Weiter" "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." - "Benutzername" diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index 0735fb7fd2..6f023f6373 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -11,10 +11,4 @@ "El servidor seleccionado no admite contraseñas ni inicio de sesión OIDC. Póngase en contacto con su administrador o elija otro homeserver." "Introduce tus datos" "¡Hola de nuevo!" - "Continuar" - "Continuar" - "Selecciona tu servidor" - "Contraseña" - "Continuar" - "Usuario" diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index a566198a45..3b7dae468c 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -37,11 +37,5 @@ Merci pour votre patience !" "Bienvenue dans %1$s !" "Vous y êtes presque." "Vous y êtes." - "Continuer" - "Continuer" - "Sélectionnez votre serveur" - "Mot de passe" - "Continuer" "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." - "Nom d’utilisateur" diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml index f7321f1d52..227b85bed7 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -11,10 +11,4 @@ "L\'homeserver selezionato non supporta la password o l\'accesso OIDC. Contatta il tuo amministratore o scegli un altro homeserver." "Inserisci i tuoi dati" "Bentornato!" - "Continua" - "Continua" - "Seleziona il tuo server" - "Password" - "Continua" - "Nome utente" diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index e780269a39..2241c63ec9 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -37,11 +37,5 @@ Vă mulțumim pentru răbdare!" "Bun venit la %1$s" "Sunteți pe lista de așteptare" "Sunteți conectat!" - "Continuați" - "Continuați" - "Selectați serverul" - "Parola" - "Continuați" "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." - "Utilizator" diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml index 641db1e0d4..733b0d93d2 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -18,6 +18,7 @@ "URL-адрес домашнего сервера" "Вы можете подключиться только к существующему серверу, поддерживающему sliding sync. Администратору домашнего сервера потребуется настроить его. %1$s" "Какой адрес у вашего сервера?" + "Выберите свой сервер" "Данная учетная запись была деактивирована." "Неверное имя пользователя и/или пароль" "Это не корректный идентификатор пользователя. Ожидаемый формат: \'@user:homeserver.org\'" @@ -37,11 +38,5 @@ "Добро пожаловать в %1$s!" "Почти готово!" "Вы зарегистрированы!" - "Продолжить" - "Продолжить" - "Выберите свой сервер" - "Пароль" - "Продолжить" "Matrix — это открытая сеть для безопасной децентрализованной связи." - "Имя пользователя" diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml index e94990554f..31656d628a 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -18,6 +18,7 @@ "Adresa URL domovského servera" "Pripojiť sa môžete len k existujúcemu serveru, ktorý podporuje kĺzavú synchronizáciu. Váš správca domovského servera ju bude musieť nakonfigurovať. %1$s" "Aká je adresa vášho servera?" + "Vyberte svoj server" "Tento účet bol deaktivovaný." "Nesprávne používateľské meno a/alebo heslo" "Toto nie je platný identifikátor používateľa. Očakávaný formát: \'@pouzivatel:homeserver.sk\'" @@ -37,11 +38,5 @@ "Vitajte v %1$s" "Ste na čakanej listine!" "Ste dnu!" - "Pokračovať" - "Pokračovať" - "Vyberte svoj server" - "Heslo" - "Pokračovať" "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." - "Používateľské meno" diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml index 789de2b30e..45f10295f3 100644 --- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml @@ -26,11 +26,5 @@ "您即將登入 %1$s" "您即將在 %1$s 建立帳號" "歡迎使用 %1$s!" - "繼續" - "繼續" - "選擇您的伺服器" - "密碼" - "繼續" "Matrix 是一個開放網路,為了安全、去中心化的通訊而生。" - "使用者名稱" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index e09f4fe693..c9797db5ac 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -18,6 +18,7 @@ "Homeserver URL" "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$s" "What is the address of your server?" + "Select your server" "This account has been deactivated." "Incorrect username and/or password" "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’" @@ -37,11 +38,5 @@ Thanks for your patience!" "Welcome to %1$s!" "You’re almost there." "You\'re in." - "Continue" - "Continue" - "Select your server" - "Password" - "Continue" "Matrix is an open network for secure, decentralised communication." - "Username" diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index 906ef6b987..7129031292 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -38,6 +38,6 @@ "Vaši zprávu se nepodařilo odeslat" "Přidat emoji" "Zobrazit méně" + "Držte pro nahrávání" "Nahrání média se nezdařilo, zkuste to prosím znovu." - "Odstranit" diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index 0249df4a9e..d08c04a785 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -38,5 +38,4 @@ "Emoji hinzufügen" "Weniger anzeigen" "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." - "Entfernen" diff --git a/features/messages/impl/src/main/res/values-es/translations.xml b/features/messages/impl/src/main/res/values-es/translations.xml index 5d41b319bd..fe186df358 100644 --- a/features/messages/impl/src/main/res/values-es/translations.xml +++ b/features/messages/impl/src/main/res/values-es/translations.xml @@ -4,5 +4,4 @@ "%1$d cambio en la sala" "%1$d cambios en la sala" - "Eliminar" diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index 5ae43b98d7..ea6f387d1a 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -38,5 +38,4 @@ "Ajouter un émoji" "Afficher moins" "Échec du traitement des médias à télécharger, veuillez réessayer." - "Supprimer" diff --git a/features/messages/impl/src/main/res/values-it/translations.xml b/features/messages/impl/src/main/res/values-it/translations.xml index 694de002fe..1b0a2c99a3 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -4,5 +4,4 @@ "%1$d modifica alla stanza" "%1$d modifiche alla stanza" - "Rimuovi" diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml index c351eb29cb..20bec7b1b3 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -39,5 +39,4 @@ "Adăugați emoji" "Afișați mai puțin" "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." - "Ștergeți" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index 5dd73b8f8f..e54b92ff08 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -39,5 +39,4 @@ "Добавить эмодзи" "Показать меньше" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." - "Удалить" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index 2c98aab148..c9129faf08 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -38,6 +38,6 @@ "Vašu správu sa nepodarilo odoslať" "Pridať emoji" "Zobraziť menej" + "Podržaním nahrajte" "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." - "Odstrániť" diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml index 5f3b2b6393..078d71bd9f 100644 --- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml @@ -26,5 +26,4 @@ "無法傳送您的訊息" "新增表情符號" "較少" - "移除" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index d904a1933a..bb285968c6 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -37,6 +37,6 @@ "Your message failed to send" "Add emoji" "Show less" + "Hold to record" "Failed processing media to upload, please try again." - "Remove" diff --git a/features/onboarding/impl/src/main/res/values-cs/translations.xml b/features/onboarding/impl/src/main/res/values-cs/translations.xml index 6b8f0eaa91..de8b00cd70 100644 --- a/features/onboarding/impl/src/main/res/values-cs/translations.xml +++ b/features/onboarding/impl/src/main/res/values-cs/translations.xml @@ -3,7 +3,6 @@ "Ruční přihlášení" "Přihlásit se pomocí QR kódu" "Vytvořit účet" - "Komunikujte a spolupracujte bezpečně" "Vítejte u dosud nejrychlejšího Elementu. Vylepšený pro rychlost a jednoduchost." "Vítejte v %1$s. Vylepšený, pro rychlost a jednoduchost." "Buďte ve svém živlu" diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml index b7f231a32a..ec802a1ba1 100644 --- a/features/onboarding/impl/src/main/res/values-de/translations.xml +++ b/features/onboarding/impl/src/main/res/values-de/translations.xml @@ -3,7 +3,6 @@ "Manuell anmelden" "Mit QR-Code anmelden" "Konto erstellen" - "Sicher kommunizieren und zusammenarbeiten" "Willkommen beim schnellsten Element aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit." "Willkommen zu %1$s. Aufgeladen, für Geschwindigkeit und Einfachheit." "Sei in deinem Element" diff --git a/features/onboarding/impl/src/main/res/values-fr/translations.xml b/features/onboarding/impl/src/main/res/values-fr/translations.xml index 789b29c5b8..94b9461a88 100644 --- a/features/onboarding/impl/src/main/res/values-fr/translations.xml +++ b/features/onboarding/impl/src/main/res/values-fr/translations.xml @@ -3,7 +3,6 @@ "Se connecter manuellement" "Se connecter avec un QR code" "Créer un compte" - "Communiquez et collaborez en toute sécurité" "Bienvenue dans l’Element le plus rapide de tous les temps. Boosté pour plus de rapidité et de simplicité." "Bienvenue sur %1$s. Boosté, pour plus de rapidité et de simplicité." "Soyez dans votre Element" diff --git a/features/onboarding/impl/src/main/res/values-ro/translations.xml b/features/onboarding/impl/src/main/res/values-ro/translations.xml index 3572d3a47f..b6db94d213 100644 --- a/features/onboarding/impl/src/main/res/values-ro/translations.xml +++ b/features/onboarding/impl/src/main/res/values-ro/translations.xml @@ -3,7 +3,6 @@ "Conectați-vă manual" "Conectați-vă cu un cod QR" "Creați un cont" - "Comunicați și colaborați în siguranță" "Bine ați venit la cel mai rapid Element din toate timpurile. Supraalimentat pentru viteză și simplitate." "Bun venit în %1$s. Supraalimentat, pentru viteză și simplitate." "Fii în Elementul tău" diff --git a/features/onboarding/impl/src/main/res/values-ru/translations.xml b/features/onboarding/impl/src/main/res/values-ru/translations.xml index 21bc36c78a..31d09cd396 100644 --- a/features/onboarding/impl/src/main/res/values-ru/translations.xml +++ b/features/onboarding/impl/src/main/res/values-ru/translations.xml @@ -3,7 +3,6 @@ "Вход в систему вручную" "Войти с помощью QR-кода" "Создать учетную запись" - "Безопасное общение и совместная работа" "Добро пожаловать в самый быстрый Element. Преимущество в скорости и простоте." "Добро пожаловать в %1$s. Supercharged — это скорость и простота." "Будь c element" diff --git a/features/onboarding/impl/src/main/res/values-sk/translations.xml b/features/onboarding/impl/src/main/res/values-sk/translations.xml index b41e671956..9639f7c903 100644 --- a/features/onboarding/impl/src/main/res/values-sk/translations.xml +++ b/features/onboarding/impl/src/main/res/values-sk/translations.xml @@ -3,7 +3,6 @@ "Prihlásiť sa manuálne" "Prihlásiť sa pomocou QR kódu" "Vytvoriť účet" - "Komunikujte a spolupracujte bezpečne" "Vitajte v najrýchlejšom Element vôbec. Nadupaný pre rýchlosť a jednoduchosť." "Vitajte v %1$s. Nadupaný, pre rýchlosť a jednoduchosť." "Buďte vo svojom elemente" diff --git a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml index 64ab9f57b3..22c9d70004 100644 --- a/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/onboarding/impl/src/main/res/values-zh-rTW/translations.xml @@ -3,7 +3,6 @@ "手動登入" "使用 QR code 登入" "建立帳號" - "安全地通訊與協作" "歡迎使用有史以來最快的 Element。速度超快,操作簡便。" "Be in your element" diff --git a/features/onboarding/impl/src/main/res/values/localazy.xml b/features/onboarding/impl/src/main/res/values/localazy.xml index cdb258cdad..2e8d8724d4 100644 --- a/features/onboarding/impl/src/main/res/values/localazy.xml +++ b/features/onboarding/impl/src/main/res/values/localazy.xml @@ -3,7 +3,6 @@ "Sign in manually" "Sign in with QR code" "Create account" - "Communicate and collaborate securely" "Welcome to the fastest Element ever. Supercharged for speed and simplicity." "Welcome to %1$s. Supercharged, for speed and simplicity." "Be in your element" 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 914228d976..efac864365 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -1,5 +1,8 @@ + "Режим разработчика" + "Предоставьте разработчикам доступ к функциям и функциональным возможностям." + "Отключить редактор форматированного текста и включить Markdown." "Отображаемое имя" "Ваше отображаемое имя" "Произошла неизвестная ошибка, изменить информацию не удалось." diff --git a/features/rageshake/api/src/main/res/values-cs/translations.xml b/features/rageshake/api/src/main/res/values-cs/translations.xml index 20d6f31ed0..13769d6d59 100644 --- a/features/rageshake/api/src/main/res/values-cs/translations.xml +++ b/features/rageshake/api/src/main/res/values-cs/translations.xml @@ -1,5 +1,4 @@ "%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?" - "Zdá se, že jste frustrovaně třásli telefonem. Chcete otevřít obrazovku pro nahlášení chyby?" diff --git a/features/rageshake/api/src/main/res/values-de/translations.xml b/features/rageshake/api/src/main/res/values-de/translations.xml index 468ac44491..6cfb4608dc 100644 --- a/features/rageshake/api/src/main/res/values-de/translations.xml +++ b/features/rageshake/api/src/main/res/values-de/translations.xml @@ -1,5 +1,4 @@ "%1$s ist bei der letzten Nutzung abgestürzt. Möchtest du einen Absturzbericht mit uns teilen?" - "Du scheinst das Telefon aus Frustration zu schütteln. Möchtest du den Bildschirm für den Fehlerbericht öffnen?" diff --git a/features/rageshake/api/src/main/res/values-fr/translations.xml b/features/rageshake/api/src/main/res/values-fr/translations.xml index 5c3571e443..2031490c1f 100644 --- a/features/rageshake/api/src/main/res/values-fr/translations.xml +++ b/features/rageshake/api/src/main/res/values-fr/translations.xml @@ -1,5 +1,4 @@ "%1$s s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?" - "Vous semblez secouer le téléphone avec frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?" diff --git a/features/rageshake/api/src/main/res/values-ru/translations.xml b/features/rageshake/api/src/main/res/values-ru/translations.xml index 6cb17a3401..d45ecb192d 100644 --- a/features/rageshake/api/src/main/res/values-ru/translations.xml +++ b/features/rageshake/api/src/main/res/values-ru/translations.xml @@ -1,5 +1,5 @@ "При последнем использовании %1$s произошел сбой. Хотите поделиться отчетом о сбое?" - "Кажется, вы трясли телефон. Хотите открыть экран отчета об ошибке?" + "Похоже, что вы трясете телефон. Хотите открыть экран сообщения об ошибке?" diff --git a/features/rageshake/api/src/main/res/values-sk/translations.xml b/features/rageshake/api/src/main/res/values-sk/translations.xml index 753ead0b97..c1d55eab34 100644 --- a/features/rageshake/api/src/main/res/values-sk/translations.xml +++ b/features/rageshake/api/src/main/res/values-sk/translations.xml @@ -1,5 +1,5 @@ "%1$s zlyhal pri poslednom použití. Chcete zdieľať správu o páde s našim tímom?" - "Zdá sa, že zúrivo trasiete telefónom. Chcete otvoriť obrazovku s nahlásením chýb?" + "Zdá sa, že zúrivo trasiete telefónom. Chcete otvoriť obrazovku s hlásením chýb?" 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 defedd3bfb..7c80ba3066 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -45,7 +45,6 @@ "Znovu uvidíte všechny zprávy od nich." "Odblokovat uživatele" "Opustit místnost" - "Lidé" "Zabezpečení" "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 1495840415..9549709ef0 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -44,7 +44,6 @@ "Du kannst dann wieder alle Nachrichten von ihnen sehen." "Benutzer entsperren" "Raum verlassen" - "Personen" "Sicherheit" "Thema" diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml index 42bce4b756..620f974340 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -15,7 +15,6 @@ "Al desbloquear al usuario, podrás volver a ver todos sus mensajes." "Desbloquear usuario" "Salir de la sala" - "Personas" "Seguridad" "Tema" 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 7efbe4c1f7..7cf727dfb5 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -44,7 +44,6 @@ "Vous pourrez à nouveau voir tous ses messages." "Débloquer l’utilisateur" "Quitter le salon" - "Personnes" "Sécurité" "Sujet" 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 190eda82ee..c00fb14bc1 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -15,7 +15,6 @@ "Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi." "Sblocca utente" "Esci dalla stanza" - "Persone" "Sicurezza" "Oggetto" 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 f86fbb8bb5..7ec53722f4 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -44,7 +44,6 @@ "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." "Deblocați utilizatorul" "Părăsiți camera" - "Persoane" "Securitate" "Subiect" 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 cff08460c1..1b2a305d3a 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -45,7 +45,6 @@ "Вы снова сможете увидеть все сообщения." "Разблокировать пользователя" "Покинуть комнату" - "Пользователи" "Безопасность" "Тема" 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 01e0f717e1..8d28fe5fe5 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -45,7 +45,6 @@ "Všetky správy od nich budete môcť opäť vidieť." "Odblokovať používateľa" "Opustiť miestnosť" - "Ľudia" "Bezpečnosť" "Téma" diff --git a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml index d8a76b50d7..4b82111269 100644 --- a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml @@ -32,7 +32,6 @@ "解除封鎖" "解除封鎖使用者" "離開聊天室" - "夥伴" "安全性" "主題" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 7f0f4ddc30..c88e2e43fa 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -44,7 +44,6 @@ "You\'ll be able to see all messages from them again." "Unblock user" "Leave room" - "People" "Security" "Topic" diff --git a/features/signedout/impl/src/main/res/values-cs/translations.xml b/features/signedout/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..06419f3cce --- /dev/null +++ b/features/signedout/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,8 @@ + + + "Změnili jste heslo v jiné relaci" + "Odstranili jste relaci z jiné relace" + "Správce vašeho serveru zrušil váš přístup" + "Je možné, že jste byli odhlášeni z některého z níže uvedených důvodů. Chcete-li pokračovat v používání %s, přihlaste se znovu." + "Jste odhlášeni" + diff --git a/features/signedout/impl/src/main/res/values-ru/translations.xml b/features/signedout/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..d3fd510afd --- /dev/null +++ b/features/signedout/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,8 @@ + + + "Вы изменили свой пароль в другой сессии" + "Вы удалили сессию из другой сессии" + "Администратор вашего сервера аннулировал ваш доступ" + "Возможно, вы вышли из системы по одной из причин, перечисленных ниже. Пожалуйста, войдите в систему еще раз, чтобы продолжить использование %s." + "Вы вышли из системы" + diff --git a/features/signedout/impl/src/main/res/values-sk/translations.xml b/features/signedout/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..1c0720b8ee --- /dev/null +++ b/features/signedout/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,8 @@ + + + "Zmenili ste heslo pri inej relácii" + "Odstránili ste reláciu z inej relácie" + "Správca vášho servera vám zrušil váš prístup" + "Možno ste boli odhlásení z jedného z nižšie uvedených dôvodov. Ak chcete pokračovať v používaní %s, prihláste sa znova." + "Ste odhlásený" + 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 6bf8db5ac0..83e2c9316f 100644 --- a/features/verifysession/impl/src/main/res/values-cs/translations.xml +++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml @@ -14,6 +14,4 @@ "Shodují se" "Pro pokračování přijměte požadavek na zahájení ověření v jiné relaci." "Čekání na přijetí žádosti" - "Ověření zrušeno" - "Začít" 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 183477a4c1..7ac1ddce6f 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -14,6 +14,4 @@ "Sie stimmen überein" "Akzeptiere die Anfrage, um den Verifizierungsprozess in deiner anderen Session zu starten, um fortzufahren." "Warten auf die Annahme der Anfrage" - "Verifizierung abgebrochen" - "Start" diff --git a/features/verifysession/impl/src/main/res/values-es/translations.xml b/features/verifysession/impl/src/main/res/values-es/translations.xml index 386ecfc37c..e80fcc8682 100644 --- a/features/verifysession/impl/src/main/res/values-es/translations.xml +++ b/features/verifysession/impl/src/main/res/values-es/translations.xml @@ -14,6 +14,4 @@ "Coinciden" "Acepta la solicitud para iniciar el proceso de verificación en tu otra sesión para continuar." "A la espera de aceptar la solicitud" - "Verificación cancelada" - "Comenzar" 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 9339d6c760..c9d3446f2a 100644 --- a/features/verifysession/impl/src/main/res/values-fr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml @@ -14,6 +14,4 @@ "Ils correspondent" "Pour continuer, acceptez la demande de lancement de la procédure de vérification dans votre autre session." "En attente d’acceptation de la demande" - "Vérification annulée" - "Démarrer" 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 7a6765adbf..852e94b463 100644 --- a/features/verifysession/impl/src/main/res/values-it/translations.xml +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -14,6 +14,4 @@ "Corrispondono" "Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare." "In attesa di accettare la richiesta" - "Verifica annullata" - "Inizia" diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml index e392438bcd..f772bd9b12 100644 --- a/features/verifysession/impl/src/main/res/values-ro/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -14,6 +14,4 @@ "Se potrivesc" "Acceptați solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua." "Se așteptă acceptarea cererii" - "Verificare anulată" - "Începeți" 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 552204fd7a..2de79c6ac2 100644 --- a/features/verifysession/impl/src/main/res/values-ru/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ru/translations.xml @@ -14,6 +14,4 @@ "Они совпадают" "Для продолжения работы примите запрос на запуск процесса проверки в другом сеансе." "Ожидание принятия запроса" - "Проверка отменена" - "Начать" 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 275924e9ec..c00a995266 100644 --- a/features/verifysession/impl/src/main/res/values-sk/translations.xml +++ b/features/verifysession/impl/src/main/res/values-sk/translations.xml @@ -14,6 +14,4 @@ "Zhodujú sa" "Ak chcete pokračovať, prijmite žiadosť o spustenie procesu overenia vo vašej druhej relácii." "Čaká sa na prijatie žiadosti" - "Overovanie zrušené" - "Spustiť" 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 14de424bcc..39a74d1bb6 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 @@ -14,6 +14,4 @@ "一樣" "準備開始驗證,請到您的其他工作階段接受請求。" "等待接受請求" - "驗證已取消" - "開始" diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index 67dc975128..cff455bcaa 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -14,6 +14,4 @@ "They match" "Accept the request to start the verification process in your other session to continue." "Waiting to accept request" - "Verification cancelled" - "Start" 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 a1e9f385f9..f28095763c 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -1,13 +1,17 @@ + "Smazat" "Skrýt heslo" "Pouze zmínky" "Ztišeno" + "Pozastavit" + "Přehrát" "Hlasování" "Hlasování ukončeno" "Odeslat soubory" "Zobrazit heslo" "Uživatelské menu" + "Nahrajte hlasovou zprávu. Dvojitě klepněte a podržte pro záznam. Uvolněním ukončíte nahrávání." "Přijmout" "Přidat na časovou osu" "Zpět" @@ -64,6 +68,7 @@ "Odeslat zprávu" "Sdílet" "Sdílet odkaz" + "Přihlásit se znovu" "Přeskočit" "Začít" "Zahájit chat" @@ -89,6 +94,7 @@ "* %1$s %2$s" "Šifrování povoleno" "Chyba" + "Všichni" "Soubor" "Soubor byl uložen do složky Stažené soubory" "Přeposlat zprávu" @@ -127,7 +133,6 @@ "Výsledky hledání" "Zabezpečená záloha" "Zabezpečení" - "Vyberte svůj server" "Odesílání…" "Server není podporován" "URL serveru" @@ -191,7 +196,6 @@ "%d hlasů" "Zatřeste zařízením pro nahlášení chyby" - "Zdá se, že jste frustrovaně třásli telefonem. Chcete otevřít obrazovku pro nahlášení chyby?" "Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy." "Důvod nahlášení tohoto obsahu" "Toto je začátek %1$s." @@ -229,7 +233,6 @@ Pokud budete pokračovat, některá nastavení se mohou změnit." "Systémová oznámení byla vypnuta" "Oznámení" "Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele" - "Účet a zařízení" "Sdílet polohu" "Sdílet moji polohu" "Otevřít v Mapách Apple" @@ -239,7 +242,6 @@ Pokud budete pokračovat, některá nastavení se mohou změnit." "Poloha" "Rageshake" "Práh detekce" - "Obecné" "Verze: %1$s (%2$s)" "en" "Chyba" 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 a123d9a8bd..dd84815254 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -126,7 +126,6 @@ "Nach jemandem suchen" "Suchergebnisse" "Sicherheit" - "Wähle deinen Server aus" "Wird gesendet…" "Server wird nicht unterstützt" "Server-URL" @@ -187,7 +186,6 @@ "%d Stimmen" "Schüttel heftig zum Melden von Fehlern" - "Du scheinst das Telefon aus Frustration zu schütteln. Möchtest du den Bildschirm für den Fehlerbericht öffnen?" "Diese Meldung wird an den Administrator deines Homeservers weitergeleitet. Dieser kann keine verschlüsselten Nachrichten lesen." "Grund für die Meldung dieses Inhalts" "Dies ist der Anfang von %1$s." @@ -221,7 +219,6 @@ "Systembenachrichtigungen deaktiviert" "Benachrichtigungen" "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest" - "Konto und Geräte" "Standort teilen" "Meinen Standort teilen" "In Apple Maps öffnen" @@ -231,7 +228,6 @@ "Standort" "Rageshake" "Erkennungsschwelle" - "Allgemein" "Version: %1$s (%2$s)" "en" "Fehler" diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index fa8a80f953..8bc500d7fe 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -77,7 +77,6 @@ "Informe enviado" "Buscar a alguien" "Seguridad" - "Selecciona tu servidor" "Enviando…" "Servidor no compatible" "Dirección del servidor" @@ -117,7 +116,6 @@ "%1$d miembros" "Agitar con fuerza para informar de un error" - "Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?" "Este mensaje se notificará al administrador de su homeserver. No podrán leer ningún mensaje cifrado." "Motivo para denunciar este contenido" "Este es el principio de %1$s." @@ -126,7 +124,6 @@ "Marque si quieres ocultar todos los mensajes actuales y futuros de este usuario" "Agitar con fuerza" "Umbral de detección" - "General" "Versión: %1$s (%2$s)" "es" "Error" 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 f05ab77402..f0de166ff1 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -126,7 +126,6 @@ "Rechercher quelqu’un" "Résultats de la recherche" "Sécurité" - "Sélectionnez votre serveur" "Envoi en cours…" "Serveur non pris en charge" "URL du serveur" @@ -187,7 +186,6 @@ "%d votes" "Rageshake pour signaler un problème" - "Vous semblez secouer le téléphone avec frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?" "Ce message sera signalé à l’administrateur de votre serveur d’accueil. Il ne pourra lire aucun message chiffré." "Raison du signalement de ce contenu" "Ceci est le début de %1$s." @@ -225,7 +223,6 @@ Si vous continuez, il est possible que certains de vos paramètres soient modifi "Les notifications du système sont désactivées" "Notifications" "Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur." - "Compte et sessions" "Partage de position" "Partager ma position" "Ouvrir dans Apple Maps" @@ -235,7 +232,6 @@ Si vous continuez, il est possible que certains de vos paramètres soient modifi "Position" "Rageshake" "Seuil de détection" - "Général" "Version : %1$s ( %2$s )" "Ang." "Erreur" diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index b15d570dfc..303ab94c50 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -77,7 +77,6 @@ "Segnalazione inviata" "Cerca qualcuno" "Sicurezza" - "Seleziona il tuo server" "Invio in corso…" "Server non supportato" "URL del server" @@ -117,7 +116,6 @@ "%1$d membri" "Scuoti per segnalare un problema" - "Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?" "Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi criptati." "Motivo della segnalazione di questo contenuto" "Questo è l\'inizio di %1$s." @@ -126,7 +124,6 @@ "Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente" "Rageshake" "Soglia di rilevamento" - "Generali" "Versione: %1$s (%2$s)" "it" "Errore" diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 91de1b2c20..566133a6a5 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -111,7 +111,6 @@ "Căutați pe cineva" "Rezultatele căutării" "Securitate" - "Selectați serverul" "Se trimite…" "Serverul nu este compatibil" "Adresa URL a serverului" @@ -171,7 +170,6 @@ "%d voturi" "Rageshake pentru a raporta erori" - "Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?" "Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat." "Motivul raportării acestui conținut" "Acesta este începutul conversației %1$s." @@ -209,7 +207,6 @@ Dacă continuați, unele dintre setările dumneavoastră pot fi modificate.""Notificările de sistem sunt dezactivate" "Notificări" "Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator" - "Cont și dispozitive" "Partajați locația" "Distribuiți locația mea" "Deschideți în Apple Maps" @@ -219,7 +216,6 @@ Dacă continuați, unele dintre setările dumneavoastră pot fi modificate.""Locație" "Rageshake" "Prag de detecție" - "General" "Versiunea: %1$s (%2$s)" "ro" "Eroare" 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 ab7cdc7e85..7401685482 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -36,6 +36,7 @@ "Пригласить друзей в %1$s" "Пригласите пользователей в %1$s" "Приглашения" + "Присоединиться" "Подробнее" "Выйти" "Покинуть комнату" @@ -63,6 +64,7 @@ "Отправить сообщение" "Поделиться" "Поделиться ссылкой" + "Повторите вход" "Пропустить" "Начать" "Начать чат " @@ -71,6 +73,7 @@ "Сделать фото" "Показать источник" "Да" + "Редактировать опрос" "О приложении" "Политика допустимого использования" "Дополнительные параметры" @@ -93,6 +96,7 @@ "GIF" "Изображения" "В ответ на %1$s" + "Установить APK" "Идентификатор Matrix ID не найден, приглашение может быть не получено." "Покинуть комнату" "Ссылка скопирована в буфер обмена" @@ -122,8 +126,8 @@ "например, название вашего проекта" "Поиск человека" "Результаты поиска" + "Безопасное резервное копирование" "Безопасность" - "Выберите свой сервер" "Отправка…" "Сервер не поддерживается" "Адрес сервера" @@ -148,7 +152,9 @@ "Проверка отменена" "Проверка завершена" "Видео" + "Голосовое сообщение" "Ожидание…" + "Вы действительно хотите завершить данный опрос?" "Опрос: %1$s" "Подтверждение" "Предупреждение" @@ -185,7 +191,6 @@ "%d голосов" "Rageshake сообщит об ошибке" - "Кажется, вы трясли телефон. Хотите открыть экран отчета об ошибке?" "Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения." "Причина, по которой вы пожаловались на этот контент" "Это начало %1$s." @@ -223,7 +228,6 @@ "Системные уведомления выключены" "Уведомления" "Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя" - "Учетная запись и устройства" "Поделиться местоположением" "Поделиться моим местоположением" "Открыть в Apple Maps" @@ -233,7 +237,6 @@ "Местоположение" "Rageshake" "Порог обнаружения" - "Основные" "Версия: %1$s (%2$s)" "en" "Ошибка" 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 4d536bc619..536db8c501 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -1,13 +1,17 @@ + "Vymazať" "Skryť heslo" "Iba zmienky" "Stlmené" + "Pozastaviť" + "Prehrať" "Anketa" "Ukončená anketa" "Odoslať súbory" "Zobraziť heslo" "Používateľské menu" + "Nahrávanie hlasovej správy. Nahrávanie spustíte dvojitým ťuknutím a podržaním. Uvoľnením nahrávanie ukončíte." "Prijať" "Pridať na časovú os" "Späť" @@ -64,6 +68,7 @@ "Odoslať správu" "Zdieľať" "Zdieľať odkaz" + "Prihláste sa znova" "Preskočiť" "Spustiť" "Začať konverzáciu" @@ -89,6 +94,7 @@ "* %1$s %2$s" "Šifrovanie zapnuté" "Chyba" + "Všetci" "Súbor" "Súbor bol uložený do priečinka Stiahnuté súbory" "Preposlať správu" @@ -101,7 +107,7 @@ "Odkaz bol skopírovaný do schránky" "Načítava sa…" "Správa" - "Rozloženie správy" + "Štýl správ" "Správa odstránená" "Moderné" "Stlmiť" @@ -127,7 +133,6 @@ "Výsledky hľadania" "Bezpečné zálohovanie" "Bezpečnosť" - "Vyberte svoj server" "Odosiela sa…" "Server nie je podporovaný" "URL adresa servera" @@ -191,7 +196,6 @@ "%d hlasov" "Zúrivo potriasť pre nahlásenie chyby" - "Zdá sa, že zúrivo trasiete telefónom. Chcete otvoriť obrazovku s nahlásením chýb?" "Táto správa bude nahlásená správcovi vášho domovského servera. Nebude môcť prečítať žiadne šifrované správy." "Dôvod nahlásenia tohto obsahu" "Toto je začiatok %1$s." @@ -229,7 +233,6 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""Systémové oznámenia sú vypnuté" "Oznámenia" "Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa" - "Účet a zariadenia" "Zdieľať polohu" "Zdieľať moju polohu" "Otvoriť v Apple Maps" @@ -239,7 +242,6 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""Poloha" "Zúrivé potrasenie" "Prahová hodnota detekcie" - "Všeobecné" "Verzia: %1$s (%2$s)" "sk" "Chyba" 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 cc1b0102d1..71aa40e8fb 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 @@ -1,8 +1,11 @@ + "刪除" "隱藏密碼" "僅限提及" "已關閉通知" + "暫停" + "播放" "投票" "投票已結束" "傳送檔案" @@ -121,7 +124,6 @@ "範例:您的計畫名稱" "搜尋結果" "安全性" - "選擇您的伺服器" "傳送中…" "伺服器不支援" "伺服器 URL" @@ -190,7 +192,6 @@ "系統設定" "已關閉系統通知" "通知" - "帳號與裝置" "分享位置" "分享我的位置" "在 Apple Maps 中開啟" @@ -198,7 +199,6 @@ "在開放街圖(OpenStreetMap) 中開啟" "分享這個位置" "位置" - "一般" "版本:%1$s(%2$s)" "zh-tw" "錯誤" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 539c215a07..b9a6460bec 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -94,6 +94,7 @@ "* %1$s %2$s" "Encryption enabled" "Error" + "Everyone" "File" "File saved to Downloads" "Forward message" @@ -132,7 +133,6 @@ "Search results" "Secure backup" "Security" - "Select your server" "Sending…" "Server not supported" "Server URL" @@ -194,7 +194,6 @@ "%d votes" "Rageshake to report bug" - "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." "Reason for reporting this content" "This is the beginning of %1$s." @@ -230,7 +229,6 @@ If you proceed, some of your settings may change." "System notifications turned off" "Notifications" "Check if you want to hide all current and future messages from this user" - "Account and devices" "Share location" "Share my location" "Open in Apple Maps" @@ -240,7 +238,6 @@ If you proceed, some of your settings may change." "Location" "Rageshake" "Detection threshold" - "General" "Version: %1$s (%2$s)" "en" "en" From 819576867d5ab8cb9792be5b4c9127ebec8119ba Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Oct 2023 10:50:16 +0200 Subject: [PATCH 039/281] Always register the pusher when application starts --- changelog.d/1481.bugfix | 1 + .../libraries/push/impl/PushersManager.kt | 25 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 changelog.d/1481.bugfix diff --git a/changelog.d/1481.bugfix b/changelog.d/1481.bugfix new file mode 100644 index 0000000000..6cb5e63d50 --- /dev/null +++ b/changelog.d/1481.bugfix @@ -0,0 +1 @@ +Always register the pusher when application starts 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 39ff4323a5..7f4ee63004 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 @@ -63,20 +63,19 @@ class PushersManager @Inject constructor( override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { val userDataStore = userPushStoreFactory.create(matrixClient.sessionId) if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { - Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher") - } else { - // Register the pusher to the server - matrixClient.pushersService().setHttpPusher( - createHttpPusher(pushKey, gateway, matrixClient.sessionId) - ).fold( - { - userDataStore.setCurrentRegisteredPushKey(pushKey) - }, - { throwable -> - Timber.tag(loggerTag.value).e(throwable, "Unable to register the pusher") - } - ) + 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") } + matrixClient.pushersService().setHttpPusher( + createHttpPusher(pushKey, gateway, matrixClient.sessionId) + ).fold( + { + userDataStore.setCurrentRegisteredPushKey(pushKey) + }, + { throwable -> + Timber.tag(loggerTag.value).e(throwable, "Unable to register the pusher") + } + ) } private suspend fun createHttpPusher( From cc2664c519d570c59afc981b81d1b7aa2c5f160f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Oct 2023 13:31:02 +0200 Subject: [PATCH 040/281] Use local SDK if the file exist --- docs/_developer_onboarding.md | 13 ++++--------- libraries/matrix/impl/build.gradle.kts | 8 ++++++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 7af9207365..a100a2beba 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -117,6 +117,10 @@ You can also have access to the aars through the [release](https://github.com/ma #### Build the SDK locally +Easiest way: run the script [./tools/sdk/build_rust_sdk.sh](./tools/sdk/build_rust_sdk.sh) and just answer the questions. + +Legacy way: + If you need to locally build the sdk-android you can use the [build](https://github.com/matrix-org/matrix-rust-components-kotlin/blob/main/scripts/build.sh) script. @@ -147,15 +151,6 @@ Troubleshooting: - If you get the error `thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', .cargo/registry/src/index.crates.io-6f17d22bba15001f/cargo-ndk-2.11.0/src/cli.rs:345:18` try updating your Cargo NDK version. In this case, 2.11.0 is too old so `cargo install cargo-ndk` to install a newer version. - If you get the error `Unsupported class file major version 64` try changing your JVM version. In this case, Java 20 is not supported in Gradle yet, so downgrade to an earlier version (Java 17 worked in this case). -Finally let the `matrix/impl` module use this aar by changing the dependencies from `libs.matrix.sdk` to `projects.libraries.rustsdk`: - -```groovy -dependencies { - api(projects.libraries.rustsdk) // <- use the local version of the sdk. Uncomment this line. - //implementation(libs.matrix.sdk) // <- use the released version. Comment this line. -} -``` - You are good to test your local rust development now! ### The Android project diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index a2b616f989..ff65a2768d 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -29,8 +29,12 @@ anvil { } dependencies { - // implementation(projects.libraries.rustsdk) - implementation(libs.matrix.sdk) + if (file("${rootDir.path}/libraries/rustsdk/matrix-rust-sdk.aar").exists()) { + println("\nNote: Using local binary of the Rust SDK.\n") + implementation(projects.libraries.rustsdk) + } else { + implementation(libs.matrix.sdk) + } implementation(projects.libraries.di) implementation(projects.libraries.androidutils) implementation(projects.libraries.network) From 9d0cfd903d1800f8becb071bb5fb0ae5d2a98d58 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Oct 2023 11:11:45 +0200 Subject: [PATCH 041/281] Add script to build the rustSdk --- tools/sdk/build_rust_sdk.sh | 80 +++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100755 tools/sdk/build_rust_sdk.sh diff --git a/tools/sdk/build_rust_sdk.sh b/tools/sdk/build_rust_sdk.sh new file mode 100755 index 0000000000..2a6b7b72d0 --- /dev/null +++ b/tools/sdk/build_rust_sdk.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +# Exit on error +set -e + +# Ask to build from local source or to clone the repository +read -p "Do you want to build the Rust SDK from local source (yes/no) default to yes? " buildLocal +buildLocal=${buildLocal:-yes} + +date=$(gdate +%Y%m%d%H%M%S) + +# Ask for the Rust SDK local source path +# if folder rustSdk/ exists, use it as default +if [ ${buildLocal} == "yes" ]; then + read -p "Please enter the path to the Rust SDK local source, default to ../matrix-rust-sdk" rustSdkPath + rustSdkPath=${rustSdkPath:-../matrix-rust-sdk/} + if [ ! -d "${rustSdkPath}" ]; then + printf "\nFolder ${rustSdkPath} does not exist. Please clone the matrix-rust-sdk repository in the folder ../matrix-rust-sdk.\n\n" + exit 0 + fi +else + read -p "Please enter the Rust SDK repository url, default to https://github.com/matrix-org/matrix-rust-sdk.git " rustSdkUrl + rustSdkUrl=${rustSdkUrl:-https://github.com/matrix-org/matrix-rust-sdk.git} + read -p "Please enter the Rust SDK branch, default to main " rustSdkBranch + rustSdkBranch=${rustSdkBranch:-main} + cd .. + git clone ${rustSdkUrl} matrix-rust-sdk-$date + cd matrix-rust-sdk-$date + git checkout ${rustSdkBranch} + rustSdkPath=$(pwd) + cd ../element-x-android +fi + + +cd ${rustSdkPath} +git status + +read -p "Will build with this version of the Rust SDK ^. Is it correct (yes/no) default to yes? " sdkCorrect +sdkCorrect=${sdkCorrect:-yes} + +if [ ${sdkCorrect} != "yes" ]; then + exit 0 +fi + +# Ask if the user wants to build the app after +read -p "Do you want to build the app after (yes/no) default to yes? " buildApp +buildApp=${buildApp:-yes} + +# If folder ../matrix-rust-components-kotlin does not exist, close the repo +if [ ! -d "../matrix-rust-components-kotlin" ]; then + printf "\nFolder ../matrix-rust-components-kotlin does not exist. Cloning the repository into ../matrix-rust-components-kotlin.\n\n" + git clone https://github.com/matrix-org/matrix-rust-components-kotlin.git ../matrix-rust-components-kotlin +fi + +printf "\nResetting matrix-rust-components-kotlin to the latest main branch...\n\n" +cd ../matrix-rust-components-kotlin +git reset --hard +git checkout main +git pull + +printf "\nBuilding the SDK...\n\n" +./scripts/build.sh -p ${rustSdkPath} -m sdk -t aarch64-linux-android -o ../element-x-android/libraries/rustsdk + +cd ../element-x-android +mv ./libraries/rustsdk/sdk-android-debug.aar ./libraries/rustsdk/matrix-rust-sdk.aar +mkdir -p ./libraries/rustsdk/sdks +cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/matrix-rust-sdk-${date}.aar + + +if [ ${buildApp} != "yes" ]; then + printf "\nBuilding the application...\n\n" + ./gradlew assembleDebug +fi + +if [ ${buildLocal} == "no" ]; then + printf "\nCleaning up...\n\n" + rm -rf ../matrix-rust-sdk-$date +fi + +printf "\nDone!\n" From 21e24990c44dceab38eae537ad101fbb0e1778be Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Oct 2023 18:29:25 +0200 Subject: [PATCH 042/281] Improve and fix build_rust_sdk.sh after PR review --- libraries/matrix/impl/build.gradle.kts | 5 +++-- tools/sdk/build_rust_sdk.sh | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index ff65a2768d..23a428c38f 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -29,11 +29,12 @@ anvil { } dependencies { + releaseImplementation(libs.matrix.sdk) if (file("${rootDir.path}/libraries/rustsdk/matrix-rust-sdk.aar").exists()) { println("\nNote: Using local binary of the Rust SDK.\n") - implementation(projects.libraries.rustsdk) + debugImplementation(projects.libraries.rustsdk) } else { - implementation(libs.matrix.sdk) + debugImplementation(libs.matrix.sdk) } implementation(projects.libraries.di) implementation(projects.libraries.androidutils) diff --git a/tools/sdk/build_rust_sdk.sh b/tools/sdk/build_rust_sdk.sh index 2a6b7b72d0..6853642218 100755 --- a/tools/sdk/build_rust_sdk.sh +++ b/tools/sdk/build_rust_sdk.sh @@ -46,7 +46,7 @@ fi read -p "Do you want to build the app after (yes/no) default to yes? " buildApp buildApp=${buildApp:-yes} -# If folder ../matrix-rust-components-kotlin does not exist, close the repo +# If folder ../matrix-rust-components-kotlin does not exist, clone the repo if [ ! -d "../matrix-rust-components-kotlin" ]; then printf "\nFolder ../matrix-rust-components-kotlin does not exist. Cloning the repository into ../matrix-rust-components-kotlin.\n\n" git clone https://github.com/matrix-org/matrix-rust-components-kotlin.git ../matrix-rust-components-kotlin @@ -58,7 +58,7 @@ git reset --hard git checkout main git pull -printf "\nBuilding the SDK...\n\n" +printf "\nBuilding the SDK for aarch64-linux-android...\n\n" ./scripts/build.sh -p ${rustSdkPath} -m sdk -t aarch64-linux-android -o ../element-x-android/libraries/rustsdk cd ../element-x-android @@ -67,7 +67,7 @@ mkdir -p ./libraries/rustsdk/sdks cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/matrix-rust-sdk-${date}.aar -if [ ${buildApp} != "yes" ]; then +if [ ${buildApp} == "yes" ]; then printf "\nBuilding the application...\n\n" ./gradlew assembleDebug fi From 412691f20a644015733fbb3549ab3f296a093284 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 08:42:33 +0200 Subject: [PATCH 043/281] Update dependency com.squareup.okhttp3:okhttp-bom to v4.12.0 (#1587) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7071a98879..947b3b2752 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -106,7 +106,7 @@ accompanist_systemui = { module = "com.google.accompanist:accompanist-systemuico squareup_seismic = "com.squareup:seismic:1.0.3" # network -network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.11.0" +network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.12.0" network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" } network_okhttp = { module = "com.squareup.okhttp3:okhttp" } network_retrofit = "com.squareup.retrofit2:retrofit:2.9.0" From c2a8e13eef1bdf4ce7a5ebb327b655cddbab5d98 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Oct 2023 21:59:41 +0200 Subject: [PATCH 044/281] Pin code storage : add way to store the pin --- .../impl/storage/EncryptedPinCodeStorage.kt | 43 ++++++++ .../features/pin/impl/storage/PinCodeStore.kt | 52 +++++++++ .../storage/SharedPreferencesPinCodeStore.kt | 100 ++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/EncryptedPinCodeStorage.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/PinCodeStore.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/SharedPreferencesPinCodeStore.kt diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/EncryptedPinCodeStorage.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/EncryptedPinCodeStorage.kt new file mode 100644 index 0000000000..ae3bd7b893 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/EncryptedPinCodeStorage.kt @@ -0,0 +1,43 @@ +/* + * 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.pin.impl.storage + +/** + * Should be implemented by any class that provides access to the encrypted PIN code. + * All methods are suspending in case there are async IO operations involved. + */ +interface EncryptedPinCodeStorage { + /** + * Returns the encrypted PIN code. + */ + suspend fun getPinCode(): String? + + /** + * Saves the encrypted PIN code to some persistable storage. + */ + suspend fun savePinCode(pinCode: String) + + /** + * Deletes the PIN code from some persistable storage. + */ + suspend fun deletePinCode() + + /** + * Returns whether the PIN code is stored or not. + */ + suspend fun hasPinCode(): Boolean +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/PinCodeStore.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/PinCodeStore.kt new file mode 100644 index 0000000000..5c54cc26f9 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/PinCodeStore.kt @@ -0,0 +1,52 @@ +/* + * 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.pin.impl.storage + +interface PinCodeStore : EncryptedPinCodeStorage { + + interface Listener { + fun onPinSetUpChange(isConfigured: Boolean) + } + + /** + * Returns the remaining PIN code attempts. When this reaches 0 the PIN code access won't be available for some time. + */ + suspend fun getRemainingPinCodeAttemptsNumber(): Int + + /** + * Should decrement the number of remaining PIN code attempts. + * @return The remaining attempts. + */ + suspend fun onWrongPin(): Int + + /** + * Resets the counter of attempts for PIN code and biometric access. + */ + suspend fun resetCounter() + + /** + * Adds a listener to be notified when the PIN code us created or removed. + */ + fun addListener(listener: Listener) + + /** + * Removes a listener to be notified when the PIN code us created or removed. + */ + fun removeListener(listener: Listener) +} + + diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/SharedPreferencesPinCodeStore.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/SharedPreferencesPinCodeStore.kt new file mode 100644 index 0000000000..fc8155352f --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/SharedPreferencesPinCodeStore.kt @@ -0,0 +1,100 @@ +/* + * 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.pin.impl.storage + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.withContext +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class SharedPreferencesPinCodeStore @Inject constructor( + private val dispatchers: CoroutineDispatchers, + private val sharedPreferences: SharedPreferences, +) : PinCodeStore { + + private val listeners = CopyOnWriteArrayList() + + override suspend fun getPinCode(): String? = withContext(dispatchers.io) { + sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null) + } + + override suspend fun savePinCode(pinCode: String) = withContext(dispatchers.io) { + sharedPreferences.edit { + putString(ENCODED_PIN_CODE_KEY, pinCode) + } + withContext(dispatchers.main) { + listeners.forEach { it.onPinSetUpChange(isConfigured = true) } + } + } + + override suspend fun deletePinCode() = withContext(dispatchers.io) { + // Also reset the counters + resetCounter() + sharedPreferences.edit { + remove(ENCODED_PIN_CODE_KEY) + } + withContext(dispatchers.main) { + listeners.forEach { it.onPinSetUpChange(isConfigured = false) } + } + } + + override suspend fun hasPinCode(): Boolean = withContext(dispatchers.io) { + sharedPreferences.contains(ENCODED_PIN_CODE_KEY) + } + + override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) { + sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) + } + + override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { + val remaining = getRemainingPinCodeAttemptsNumber() - 1 + sharedPreferences.edit { + putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining) + } + remaining + } + + override suspend fun resetCounter() = withContext(dispatchers.io) { + sharedPreferences.edit { + remove(REMAINING_PIN_CODE_ATTEMPTS_KEY) + remove(REMAINING_BIOMETRICS_ATTEMPTS_KEY) + } + } + + override fun addListener(listener: PinCodeStore.Listener) { + listeners.add(listener) + } + + override fun removeListener(listener: PinCodeStore.Listener) { + listeners.remove(listener) + } + + companion object { + private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY" + private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY" + private const val REMAINING_BIOMETRICS_ATTEMPTS_KEY = "REMAINING_BIOMETRICS_ATTEMPTS_KEY" + + private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 + } +} From 85102e379378006b765915ffe4febf7db06e3c17 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Oct 2023 14:04:57 +0200 Subject: [PATCH 045/281] Crypto: add a small cryptography library module with CipherFactory --- gradle/libs.versions.toml | 10 +++ libraries/cryptography/api/build.gradle.kts | 23 ++++++ .../cryptography/api/CipherFactory.kt | 40 ++++++++++ libraries/cryptography/impl/build.gradle.kts | 35 +++++++++ .../impl/KeyStoreCipherFactory.kt | 76 +++++++++++++++++++ settings.gradle.kts | 2 + 6 files changed, 186 insertions(+) create mode 100644 libraries/cryptography/api/build.gradle.kts create mode 100644 libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CipherFactory.kt create mode 100644 libraries/cryptography/impl/build.gradle.kts create mode 100644 libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreCipherFactory.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7071a98879..15cf29dd4f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,6 +57,11 @@ autoservice = "1.1.1" # quality detekt = "1.23.1" dependencygraph = "0.12" +junit = "4.13.2" +androidx-test-ext-junit = "1.1.5" +espresso-core = "3.5.1" +appcompat = "1.6.1" +material = "1.9.0" [libraries] # Project @@ -184,6 +189,11 @@ google_autoservice_annotations = { module = "com.google.auto.service:auto-servic # value of `composecompiler` (which is used to set composeOptions.kotlinCompilerExtensionVersion. # See https://github.com/renovatebot/renovate/issues/18354 android_composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composecompiler" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [bundles] diff --git a/libraries/cryptography/api/build.gradle.kts b/libraries/cryptography/api/build.gradle.kts new file mode 100644 index 0000000000..e8cee5dbd6 --- /dev/null +++ b/libraries/cryptography/api/build.gradle.kts @@ -0,0 +1,23 @@ +/* + * 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.cryptography.api" +} diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CipherFactory.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CipherFactory.kt new file mode 100644 index 0000000000..0eec6e9fa9 --- /dev/null +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CipherFactory.kt @@ -0,0 +1,40 @@ +/* + * 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.cryptography.api + +import javax.crypto.Cipher + +/** + * Factory to create [Cipher] instances for encryption and decryption. + * The implementation should use a secure way to store the keys. + */ +interface CipherFactory { + /** + * Create a [Cipher] instance for encryption. + * @param alias the alias of the key used for encryption. + * @return the [Cipher] instance. + */ + fun createEncryptionCipher(alias: String): Cipher + + /** + * Create a [Cipher] instance for decryption. + * @param alias the alias of the key used for encryption. + * @param initializationVector the initialization vector used for encryption. + * @return the [Cipher] instance. + */ + fun createDecryptionCipher(alias: String, initializationVector: ByteArray): Cipher +} diff --git a/libraries/cryptography/impl/build.gradle.kts b/libraries/cryptography/impl/build.gradle.kts new file mode 100644 index 0000000000..fa6f9db7ea --- /dev/null +++ b/libraries/cryptography/impl/build.gradle.kts @@ -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. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.cryptography.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(projects.anvilannotations) + implementation(projects.libraries.di) + implementation(projects.libraries.cryptography.api) +} diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreCipherFactory.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreCipherFactory.kt new file mode 100644 index 0000000000..eb88b66239 --- /dev/null +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreCipherFactory.kt @@ -0,0 +1,76 @@ +/* + * 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.cryptography.impl + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.cryptography.api.CipherFactory +import io.element.android.libraries.di.AppScope +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import javax.inject.Inject + +private const val ANDROID_KEYSTORE = "AndroidKeyStore" +private const val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM +private const val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE +private const val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES +private const val ENCRYPTION_AES_TRANSFORMATION = "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING" + +/** + * Implementation of [CipherFactory] that uses the Android Keystore to store the keys. + */ +@ContributesBinding(AppScope::class) +class KeyStoreCipherFactory @Inject constructor() : CipherFactory { + + override fun createEncryptionCipher(alias: String): Cipher { + val cipher = Cipher.getInstance(ENCRYPTION_AES_TRANSFORMATION) + val secretKey = getOrGenerateKeyForAlias(alias) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + return cipher + } + + override fun createDecryptionCipher(alias: String, initializationVector: ByteArray): Cipher { + val cipher = Cipher.getInstance(ENCRYPTION_AES_TRANSFORMATION) + val secretKey = getOrGenerateKeyForAlias(alias) + val spec = GCMParameterSpec(128, initializationVector) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + return cipher + } + + private fun getOrGenerateKeyForAlias(alias: String): SecretKey { + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) + ?.secretKey + return if (secretKeyEntry == null) { + val generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, ANDROID_KEYSTORE) + val keyGenSpec = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(ENCRYPTION_BLOCK_MODE) + .setEncryptionPaddings(ENCRYPTION_PADDING) + .setKeySize(128) + .build() + generator.init(keyGenSpec) + generator.generateKey() + } else secretKeyEntry + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 105befcd04..590089adc6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,7 @@ import java.net.URI +include(":libraries:cryptography:api") + /* * Copyright (c) 2022 New Vector Ltd * From eadaa2f65cd1d69330e081b40ac39c1d56618839 Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 17 Oct 2023 16:08:08 +0100 Subject: [PATCH 046/281] List user define room notification settings - List user define room notification settings - Add new user defined style of the room notification settings view - Add navigation to expose room notification settings ui to the global settings - Add Progress indicators - Improve error handing --- .../android/appnav/LoggedInFlowNode.kt | 4 + .../android/appnav/room/RoomLoadedFlowNode.kt | 7 + .../preferences/api/PreferencesEntryPoint.kt | 2 + .../preferences/impl/PreferencesFlowNode.kt | 8 +- .../NotificationSettingsEvents.kt | 1 + .../NotificationSettingsPresenter.kt | 25 +++- .../NotificationSettingsState.kt | 2 + .../NotificationSettingsStateProvider.kt | 2 + .../notifications/NotificationSettingsView.kt | 15 ++ .../EditDefaultNotificationSettingNode.kt | 14 +- ...EditDefaultNotificationSettingPresenter.kt | 32 +++-- .../EditDefaultNotificationSettingState.kt | 2 + ...itDefaultNotificationSettingStateEvents.kt | 1 + .../EditDefaultNotificationSettingView.kt | 86 +++++++----- ...efaultNotificationSettingsStateProvider.kt | 2 + .../roomdetails/api/RoomDetailsEntryPoint.kt | 3 + .../impl/DefaultRoomDetailsEntryPoint.kt | 1 + .../roomdetails/impl/RoomDetailsFlowNode.kt | 15 +- .../RoomNotificationSettingsEvents.kt | 2 + .../RoomNotificationSettingsNode.kt | 26 +++- .../RoomNotificationSettingsPresenter.kt | 34 +++-- .../RoomNotificationSettingsState.kt | 4 + .../RoomNotificationSettingsStateProvider.kt | 4 + .../RoomNotificationSettingsView.kt | 25 +++- ...UserDefinedRoomNotificationSettingsView.kt | 128 ++++++++++++++++++ 25 files changed, 375 insertions(+), 70 deletions(-) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt 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 164c2ae2e4..065cfaafc2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -244,6 +244,10 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onVerifyClicked() { backstack.push(NavTarget.VerifySession) } + + override fun onOpenRoomNotificationSettings(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomNotificationSettings)) + } } preferencesEntryPoint.nodeBuilder(this, buildContext) .callback(callback) 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 34c459f979..2b063a2cbf 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 @@ -142,6 +142,10 @@ class RoomLoadedFlowNode @AssistedInject constructor( val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(navTarget.userId)) roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList()) } + NavTarget.RoomNotificationSettings -> { + val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings) + roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList()) + } } } @@ -154,6 +158,9 @@ class RoomLoadedFlowNode @AssistedInject constructor( @Parcelize data class RoomMemberDetails(val userId: UserId) : NavTarget + + @Parcelize + data object RoomNotificationSettings : NavTarget } @Composable 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 3d1a516593..50a605efe4 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 @@ -20,6 +20,7 @@ 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 PreferencesEntryPoint : FeatureEntryPoint { @@ -33,5 +34,6 @@ interface PreferencesEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onOpenBugReport() fun onVerifyClicked() + fun onOpenRoomNotificationSettings(roomId: RoomId) } } 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 6cf0390db2..8d77757527 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 @@ -43,6 +43,7 @@ import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler 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 kotlinx.parcelize.Parcelize @@ -152,8 +153,13 @@ class PreferencesFlowNode @AssistedInject constructor( createNode(buildContext, listOf(notificationSettingsCallback)) } is NavTarget.EditDefaultNotificationSetting -> { + val callback = object : EditDefaultNotificationSettingNode.Callback { + override fun openRoomNotificationSettings(roomId: RoomId) { + plugins().forEach { it.onOpenRoomNotificationSettings(roomId) } + } + } val input = EditDefaultNotificationSettingNode.Inputs(navTarget.isOneToOne) - createNode(buildContext, plugins = listOf(input)) + createNode(buildContext, plugins = listOf(input, callback)) } NavTarget.AdvancedSettings -> { createNode(buildContext) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt index 374b8078ca..9e87675b3a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt @@ -24,4 +24,5 @@ sealed interface NotificationSettingsEvents { data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents data object FixConfigurationMismatch : NotificationSettingsEvents data object ClearConfigurationMismatchError : NotificationSettingsEvents + data object ClearNotificationChangeError : NotificationSettingsEvents } 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 697d5887f0..689cca8f66 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 @@ -23,7 +23,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode @@ -50,6 +52,7 @@ class NotificationSettingsPresenter @Inject constructor( val systemNotificationsEnabled: MutableState = remember { mutableStateOf(systemNotificationsEnabledProvider.notificationsEnabled()) } + val changeNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } val localCoroutineScope = rememberCoroutineScope() val appNotificationsEnabled = userPushStore @@ -67,8 +70,12 @@ class NotificationSettingsPresenter @Inject constructor( fun handleEvents(event: NotificationSettingsEvents) { when (event) { - is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled) - is NotificationSettingsEvents.SetCallNotificationsEnabled -> localCoroutineScope.setCallNotificationsEnabled(event.enabled) + is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> { + localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled, changeNotificationSettingAction) + } + is NotificationSettingsEvents.SetCallNotificationsEnabled -> { + localCoroutineScope.setCallNotificationsEnabled(event.enabled, changeNotificationSettingAction) + } is NotificationSettingsEvents.SetNotificationsEnabled -> localCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled) NotificationSettingsEvents.ClearConfigurationMismatchError -> { matrixSettings.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false) @@ -77,6 +84,7 @@ class NotificationSettingsPresenter @Inject constructor( NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> { systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled() } + NotificationSettingsEvents.ClearNotificationChangeError -> changeNotificationSettingAction.value = Async.Uninitialized } } @@ -86,6 +94,7 @@ class NotificationSettingsPresenter @Inject constructor( systemNotificationsEnabled = systemNotificationsEnabled.value, appNotificationsEnabled = appNotificationsEnabled.value ), + changeNotificationSettingAction = changeNotificationSettingAction.value, eventSink = ::handleEvents ) } @@ -154,12 +163,16 @@ class NotificationSettingsPresenter @Inject constructor( ) } - private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean) = launch { - notificationSettingsService.setRoomMentionEnabled(enabled) + private fun CoroutineScope.setAtRoomNotificationsEnabled(enabled: Boolean, action: MutableState>) = launch { + suspend { + notificationSettingsService.setRoomMentionEnabled(enabled).getOrThrow() + }.runCatchingUpdatingState(action) } - private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean) = launch { - notificationSettingsService.setCallEnabled(enabled) + private fun CoroutineScope.setCallNotificationsEnabled(enabled: Boolean, action: MutableState>) = launch { + suspend { + notificationSettingsService.setCallEnabled(enabled).getOrThrow() + }.runCatchingUpdatingState(action) } private fun CoroutineScope.setNotificationsEnabled(userPushStore: UserPushStore, enabled: Boolean) = launch { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt index cf3cf6e3d0..2b0faa110c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt @@ -17,12 +17,14 @@ package io.element.android.features.preferences.impl.notifications import androidx.compose.runtime.Immutable +import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.RoomNotificationMode @Immutable data class NotificationSettingsState( val matrixSettings: MatrixSettings, val appSettings: AppSettings, + val changeNotificationSettingAction: Async, val eventSink: (NotificationSettingsEvents) -> Unit, ) { sealed interface MatrixSettings { 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 1e653c47e0..cfff59e905 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 @@ -17,6 +17,7 @@ package io.element.android.features.preferences.impl.notifications import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.RoomNotificationMode open class NotificationSettingsStateProvider : PreviewParameterProvider { @@ -37,5 +38,6 @@ fun aNotificationSettingsState() = NotificationSettingsState( systemNotificationsEnabled = false, appNotificationsEnabled = true, ), + changeNotificationSettingAction = Async.Uninitialized, 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 dd3aba842d..c7bcbb573c 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 @@ -36,6 +36,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch @@ -91,6 +93,19 @@ fun NotificationSettingsView( // onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) }, ) } + when (state.changeNotificationSettingAction) { + is Async.Loading -> { + ProgressDialog() + } + is Async.Failure -> { + ErrorDialog( + title = stringResource(CommonStrings.dialog_title_error), + content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode), + onDismiss = { state.eventSink(NotificationSettingsEvents.ClearNotificationChangeError) }, + ) + } + else -> Unit + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt index 6c4fd646f4..535203e35e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingNode.kt @@ -21,12 +21,14 @@ 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.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(SessionScope::class) class EditDefaultNotificationSettingNode @AssistedInject constructor( @@ -35,20 +37,30 @@ class EditDefaultNotificationSettingNode @AssistedInject constructor( presenterFactory: EditDefaultNotificationSettingPresenter.Factory ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun openRoomNotificationSettings(roomId: RoomId) + } + data class Inputs( val isOneToOne: Boolean ) : NodeInputs private val inputs = inputs() + private val callbacks = plugins() private val presenter = presenterFactory.create(inputs.isOneToOne) + private fun openRoomNotificationSettings(roomId: RoomId) { + callbacks.forEach { it.openRoomNotificationSettings(roomId) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() EditDefaultNotificationSettingView( state = state, + openRoomNotificationSettings = { openRoomNotificationSettings(it) }, onBackPressed = ::navigateUp, - modifier = modifier + modifier = modifier, ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt index 58140cb35f..79201e27d3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -25,7 +25,9 @@ import androidx.compose.runtime.rememberCoroutineScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode @@ -57,6 +59,8 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( mutableStateOf(null) } + val changeNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val roomsWithUserDefinedMode: MutableState> = remember { mutableStateOf(listOf()) } @@ -70,7 +74,10 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( fun handleEvents(event: EditDefaultNotificationSettingStateEvents) { when (event) { - is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> localCoroutineScope.setDefaultNotificationMode(event.mode) + is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> { + localCoroutineScope.setDefaultNotificationMode(event.mode, changeNotificationSettingAction) + } + EditDefaultNotificationSettingStateEvents.ClearError -> changeNotificationSettingAction.value = Async.Uninitialized } } @@ -78,6 +85,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( isOneToOne = isOneToOne, mode = mode.value, roomsWithUserDefinedMode = roomsWithUserDefinedMode.value, + changeNotificationSettingAction = changeNotificationSettingAction.value, eventSink = ::handleEvents ) } @@ -105,9 +113,13 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( .launchIn(this) } - private fun CoroutineScope.updateRoomsWithUserDefinedMode(summaries: List, roomsWithUserDefinedMode: MutableState>) = launch { - val roomWithUserDefinedRules = notificationSettingsService.getRoomsWithUserDefinedRules().getOrThrow().toSet() - roomsWithUserDefinedMode.value = summaries + private fun CoroutineScope.updateRoomsWithUserDefinedMode( + summaries: List, + roomsWithUserDefinedMode: MutableState> + ) = launch { + val roomWithUserDefinedRules: Set = notificationSettingsService.getRoomsWithUserDefinedRules().getOrThrow().toSet() + + val sortedSummaries = summaries .filterIsInstance() .filter { val room = matrixClient.getRoom(it.details.roomId) ?: return@filter false @@ -115,12 +127,16 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( } // locale sensitive sorting .sortedWith(compareBy(Collator.getInstance()){ it.details.name }) + + roomsWithUserDefinedMode.value = sortedSummaries } - private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode) = launch { - // On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did). - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne) - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne) + private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState>) = launch { + suspend { + // On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did). + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne).getOrThrow() + notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne).getOrThrow() + }.runCatchingUpdatingState(action) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt index e4d18239cd..e8590ec27f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt @@ -16,6 +16,7 @@ package io.element.android.features.preferences.impl.notifications.edit +import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomSummary @@ -23,5 +24,6 @@ data class EditDefaultNotificationSettingState( val isOneToOne: Boolean, val mode: RoomNotificationMode?, val roomsWithUserDefinedMode: List, + val changeNotificationSettingAction: Async, val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt index 75c9b6c1a4..f5774f1d78 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt @@ -20,4 +20,5 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode sealed interface EditDefaultNotificationSettingStateEvents { data class SetNotificationMode(val mode: RoomNotificationMode): EditDefaultNotificationSettingStateEvents + data object ClearError: EditDefaultNotificationSettingStateEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt index 56cf3acf9a..cda19e15bb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt @@ -17,19 +17,17 @@ package io.element.android.features.preferences.impl.notifications.edit import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectableGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.preferences.impl.notifications.NotificationSettingsState -import io.element.android.features.preferences.impl.notifications.NotificationSettingsStateProvider -import io.element.android.features.preferences.impl.notifications.NotificationSettingsView +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.components.ProgressDialog 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.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceView @@ -37,6 +35,7 @@ import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.ui.strings.CommonStrings @@ -47,11 +46,12 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun EditDefaultNotificationSettingView( state: EditDefaultNotificationSettingState, + openRoomNotificationSettings:(roomId: RoomId) -> Unit, onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { - val title = if(state.isOneToOne) { + val title = if (state.isOneToOne) { CommonStrings.screen_notification_settings_direct_chats } else { CommonStrings.screen_notification_settings_group_chats @@ -65,7 +65,7 @@ fun EditDefaultNotificationSettingView( // Only ALL_MESSAGES and MENTIONS_AND_KEYWORDS_ONLY are valid global defaults. val validModes = listOf(RoomNotificationMode.ALL_MESSAGES, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) - val categoryTitle = if(state.isOneToOne) { + val categoryTitle = if (state.isOneToOne) { CommonStrings.screen_notification_settings_edit_screen_direct_section_header } else { CommonStrings.screen_notification_settings_edit_screen_group_section_header @@ -84,46 +84,64 @@ fun EditDefaultNotificationSettingView( } } } - if(state.roomsWithUserDefinedMode.isNotEmpty()) { + if (state.roomsWithUserDefinedMode.isNotEmpty()) { PreferenceCategory(title = stringResource(id = CommonStrings.screen_notification_settings_edit_custom_settings_section_title)) { - LazyColumn { - items(state.roomsWithUserDefinedMode) { summary -> - val subtitle = when (summary.details.notificationMode) { - RoomNotificationMode.ALL_MESSAGES -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_all_messages) - RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_mentions_and_keywords) - RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute) - null -> "" + state.roomsWithUserDefinedMode.forEach { summary -> + val subtitle = when (summary.details.notificationMode) { + RoomNotificationMode.ALL_MESSAGES -> stringResource(id = CommonStrings.screen_notification_settings_edit_mode_all_messages) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> { + stringResource(id = CommonStrings.screen_notification_settings_edit_mode_mentions_and_keywords) } - val avatarData = AvatarData( - id = summary.identifier(), - name = summary.details.name, - url = summary.details.avatarURLString, - size = AvatarSize.CustomRoomNotificationSetting, - ) - ListItem( - headlineContent = { - Text(text = summary.details.name) - }, - supportingContent = { - Text(text = subtitle) - }, - leadingContent = ListItemContent.Custom { - Avatar(avatarData = avatarData) - } - ) + RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute) + null -> "" } + val avatarData = AvatarData( + id = summary.identifier(), + name = summary.details.name, + url = summary.details.avatarURLString, + size = AvatarSize.CustomRoomNotificationSetting, + ) + ListItem( + headlineContent = { + Text(text = summary.details.name) + }, + supportingContent = { + Text(text = subtitle) + }, + leadingContent = ListItemContent.Custom { + Avatar(avatarData = avatarData) + }, + onClick = { + openRoomNotificationSettings(summary.details.roomId) + } + ) } } } - + when (state.changeNotificationSettingAction) { + is Async.Loading -> { + ProgressDialog() + } + is Async.Failure -> { + ErrorDialog( + title = stringResource(CommonStrings.dialog_title_error), + content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode), + onDismiss = { state.eventSink(EditDefaultNotificationSettingStateEvents.ClearError) }, + ) + } + else -> Unit + } } } @DayNightPreviews @Composable -internal fun EditDefaultNotificationSettingViewPreview(@PreviewParameter(EditDefaultNotificationSettingsStateProvider::class) state: EditDefaultNotificationSettingState) = ElementPreview { +internal fun EditDefaultNotificationSettingViewPreview( + @PreviewParameter(EditDefaultNotificationSettingsStateProvider::class) state: EditDefaultNotificationSettingState +) = ElementPreview { EditDefaultNotificationSettingView( state = state, + openRoomNotificationSettings = {}, onBackPressed = {}, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt index 738074e9e6..6910b581bb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.preferences.impl.notifications.edit import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomSummary @@ -33,6 +34,7 @@ fun anEditDefaultNotificationSettingsState() = EditDefaultNotificationSettingSta isOneToOne = false, mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, roomsWithUserDefinedMode = listOf(aRoomSummary()), + changeNotificationSettingAction = Async.Uninitialized, eventSink = {} ) diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index 4fa5c18b2e..ed1e6f5c5a 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -33,6 +33,9 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : InitialTarget + + @Parcelize + data object RoomNotificationSettings : InitialTarget } data class Inputs(val initialElement: InitialTarget) : NodeInputs diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt index be6b915212..8cd6cb54d6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt @@ -42,4 +42,5 @@ class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint internal fun InitialTarget.toNavTarget() = when (this) { is InitialTarget.RoomDetails -> NavTarget.RoomDetails is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId) + is InitialTarget.RoomNotificationSettings -> NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = true) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 675ef7de60..5d7539e626 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -68,7 +68,13 @@ class RoomDetailsFlowNode @AssistedInject constructor( data object InviteMembers : NavTarget @Parcelize - object RoomNotificationSettings : NavTarget + data class RoomNotificationSettings( + /** + * When presented from oursite the context of the room, the rooms settings UI is different. + * Figma designs: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=5199-198932&mode=design&t=fTTvpuxYFjewYQOe-0 + */ + val showUserDefinedSettingStyle: Boolean + ) : NavTarget @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget @@ -91,7 +97,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( } override fun openRoomNotificationSettings() { - backstack.push(NavTarget.RoomNotificationSettings) + backstack.push(NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = false)) } } createNode(buildContext, listOf(roomDetailsCallback)) @@ -118,8 +124,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( createNode(buildContext) } - NavTarget.RoomNotificationSettings -> { - createNode(buildContext) + is NavTarget.RoomNotificationSettings -> { + val plugins = listOf(RoomNotificationSettingsNode.RoomNotificationSettingInput(navTarget.showUserDefinedSettingStyle)) + createNode(buildContext, plugins) } is NavTarget.RoomMemberDetails -> { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt index bbe756b154..c69896a98b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt @@ -21,4 +21,6 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode sealed interface RoomNotificationSettingsEvents { data class RoomNotificationModeChanged(val mode: RoomNotificationMode) : RoomNotificationSettingsEvents data class SetNotificationMode(val isDefault: Boolean): RoomNotificationSettingsEvents + data object DeleteCustomNotification: RoomNotificationSettingsEvents + data object ClearError: RoomNotificationSettingsEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt index 224f850e28..cb0168a42b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt @@ -26,6 +26,8 @@ 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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope import io.element.android.services.analytics.api.AnalyticsService @@ -37,6 +39,12 @@ class RoomNotificationSettingsNode @AssistedInject constructor( private val analyticsService: AnalyticsService, ) : Node(buildContext, plugins = plugins) { + data class RoomNotificationSettingInput( + val showUserDefinedSettingStyle: Boolean + ) : NodeInputs + + private val inputs = inputs() + init { lifecycle.subscribe( onResume = { @@ -48,10 +56,18 @@ class RoomNotificationSettingsNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() - RoomNotificationSettingsView( - state = state, - modifier = modifier, - onBackPressed = this::navigateUp, - ) + if(inputs.showUserDefinedSettingStyle) { + UserDefinedRoomNotificationSettingsView( + state = state, + modifier = modifier, + onBackPressed = this::navigateUp, + ) + } else { + RoomNotificationSettingsView( + state = state, + modifier = modifier, + onBackPressed = this::navigateUp, + ) + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt index a6e2477bcc..9fb0c8b1c8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -22,9 +22,12 @@ 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 io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomNotificationMode @@ -42,16 +45,18 @@ class RoomNotificationSettingsPresenter @Inject constructor( private val room: MatrixRoom, private val notificationSettingsService: NotificationSettingsService, ) : Presenter { - @Composable override fun present(): RoomNotificationSettingsState { val defaultRoomNotificationMode: MutableState = rememberSaveable { mutableStateOf(null) } val localCoroutineScope = rememberCoroutineScope() + val changeNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val deleteCustomNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } LaunchedEffect(Unit) { getDefaultRoomNotificationMode(defaultRoomNotificationMode) + room.updateRoomNotificationSettings() observeNotificationSettings() } @@ -60,23 +65,32 @@ class RoomNotificationSettingsPresenter @Inject constructor( fun handleEvents(event: RoomNotificationSettingsEvents) { when (event) { is RoomNotificationSettingsEvents.RoomNotificationModeChanged -> { - localCoroutineScope.setRoomNotificationMode(event.mode) + localCoroutineScope.setRoomNotificationMode(event.mode, changeNotificationSettingAction) } is RoomNotificationSettingsEvents.SetNotificationMode -> { if (event.isDefault) { - localCoroutineScope.restoreDefaultRoomNotificationMode() + localCoroutineScope.restoreDefaultRoomNotificationMode(changeNotificationSettingAction) } else { defaultRoomNotificationMode.value?.let { - localCoroutineScope.setRoomNotificationMode(it) + localCoroutineScope.setRoomNotificationMode(it, changeNotificationSettingAction) } } } + is RoomNotificationSettingsEvents.DeleteCustomNotification -> { + localCoroutineScope.restoreDefaultRoomNotificationMode(deleteCustomNotificationSettingAction) + } + RoomNotificationSettingsEvents.ClearError -> { + changeNotificationSettingAction.value = Async.Uninitialized + } } } return RoomNotificationSettingsState( + roomName = room.displayName, roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), defaultRoomNotificationMode = defaultRoomNotificationMode.value, + changeNotificationSettingAction = changeNotificationSettingAction.value, + deleteCustomNotificationSettingAction = deleteCustomNotificationSettingAction.value, eventSink = ::handleEvents, ) } @@ -98,11 +112,15 @@ class RoomNotificationSettingsPresenter @Inject constructor( ).getOrThrow() } - private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode) = launch { - notificationSettingsService.setRoomNotificationMode(room.roomId, mode) + private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode, action: MutableState>) = launch { + suspend { + notificationSettingsService.setRoomNotificationMode(room.roomId, mode).getOrThrow() + }.runCatchingUpdatingState(action) } - private fun CoroutineScope.restoreDefaultRoomNotificationMode() = launch { - notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId) + private fun CoroutineScope.restoreDefaultRoomNotificationMode(action: MutableState>) = launch { + suspend { + notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId).getOrThrow() + }.runCatchingUpdatingState(action) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt index 04742781b5..a7c5c3b883 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt @@ -16,11 +16,15 @@ package io.element.android.features.roomdetails.impl.notificationsettings +import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings data class RoomNotificationSettingsState( + val roomName: String, val roomNotificationSettings: RoomNotificationSettings?, val defaultRoomNotificationMode: RoomNotificationMode?, + val changeNotificationSettingAction: Async, + val deleteCustomNotificationSettingAction: Async, val eventSink: (RoomNotificationSettingsEvents) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt index df1dd7977b..220d82f6b5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomdetails.impl.notificationsettings import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings @@ -24,10 +25,13 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider< override val values: Sequence get() = sequenceOf( RoomNotificationSettingsState( + roomName = "Room 1", RoomNotificationSettings( mode = RoomNotificationMode.MUTE, isDefault = true), RoomNotificationMode.ALL_MESSAGES, + changeNotificationSettingAction = Async.Uninitialized, + deleteCustomNotificationSettingAction = Async.Uninitialized, eventSink = { }, ), ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt index 5cf4adb84f..92bfe0c084 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -18,7 +18,6 @@ package io.element.android.features.roomdetails.impl.notificationsettings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -30,8 +29,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.bool.orTrue +import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceText @@ -45,7 +47,6 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings -@OptIn(ExperimentalLayoutApi::class) @Composable fun RoomNotificationSettingsView( state: RoomNotificationSettingsState, @@ -74,7 +75,6 @@ fun RoomNotificationSettingsView( null -> "" } - PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) { PreferenceSwitch( isChecked = state.roomNotificationSettings?.isDefault.orTrue(), @@ -102,6 +102,16 @@ fun RoomNotificationSettingsView( ) } } + + when (state.changeNotificationSettingAction) { + is Async.Loading -> { + ProgressDialog() + } + is Async.Failure -> { + ShowChangeNotificationSettingError(state) + } + else -> Unit + } } } } @@ -144,6 +154,15 @@ fun RoomNotificationSettingsOptions( } } +@Composable +fun ShowChangeNotificationSettingError(state: RoomNotificationSettingsState) { + ErrorDialog( + title = stringResource(CommonStrings.dialog_title_error), + content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode), + onDismiss = { state.eventSink(RoomNotificationSettingsEvents.ClearError) }, + ) +} + @DayNightPreviews @Composable internal fun RoomNotificationSettingsPreview( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt new file mode 100644 index 0000000000..6435e64497 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt @@ -0,0 +1,128 @@ +/* + * 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.roomdetails.impl.notificationsettings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.preferences.PreferenceText +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 + +@Composable +fun UserDefinedRoomNotificationSettingsView( + state: RoomNotificationSettingsState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, +) { + Scaffold( + modifier = modifier, + topBar = { + UserDefinedRoomNotificationSettingsTopBar( + roomName = state.roomName, + onBackPressed = { onBackPressed() } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(padding) + .consumeWindowInsets(padding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (state.roomNotificationSettings != null) { + RoomNotificationSettingsOptions( + selected = state.roomNotificationSettings.mode, + enabled = !state.roomNotificationSettings.isDefault, + onOptionSelected = { + state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) + }, + ) + } + + PreferenceText( + title = stringResource(R.string.screen_room_notification_settings_edit_remove_setting), + icon = ImageVector.vectorResource(VectorIcons.Delete), + tintColor = MaterialTheme.colorScheme.error, + onClick = { + state.eventSink(RoomNotificationSettingsEvents.DeleteCustomNotification) + } + ) + + when (state.changeNotificationSettingAction) { + is Async.Loading -> { + ProgressDialog() + } + is Async.Failure -> { + ShowChangeNotificationSettingError(state) + } + else -> Unit + } + + when (state.deleteCustomNotificationSettingAction) { + is Async.Loading -> { + ProgressDialog() + } + is Async.Failure -> { + ShowChangeNotificationSettingError(state) + } + is Async.Success -> { + LaunchedEffect(state.deleteCustomNotificationSettingAction) { + onBackPressed() + } + } + else -> Unit + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UserDefinedRoomNotificationSettingsTopBar( + roomName: String, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, +) { + TopAppBar( + modifier = modifier, + title = { + Text( + text = roomName, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + ) +} From 895a5332f26115d6635954cb1ba81fb00917959e Mon Sep 17 00:00:00 2001 From: David Langley Date: Tue, 17 Oct 2023 16:08:35 +0100 Subject: [PATCH 047/281] Add tests --- ...faultNotificationSettingsPresenterTests.kt | 48 +++++++++++++++-- .../RoomNotificationSettingsPresenterTests.kt | 51 ++++++++++++++++--- .../FakeNotificationSettingsService.kt | 13 ++++- 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt index e8c9ff4fe5..8376aa95c3 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt @@ -23,7 +23,13 @@ import com.google.common.truth.Truth import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail +import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.test.runTest import org.junit.Test @@ -32,7 +38,7 @@ class EditDefaultNotificationSettingsPresenterTests { @Test fun `present - ensures initial state is correct`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - val presenter = EditDefaultNotificationSettingPresenter(notificationSettingsService = notificationSettingsService, isOneToOne = false) + val presenter = createPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -47,10 +53,32 @@ class EditDefaultNotificationSettingsPresenterTests { } } + @Test + fun `present - ensure list of rooms with user defined mode`() = runTest { + val room = FakeMatrixRoom() + val notificationSettingsService = FakeNotificationSettingsService( + initialRoomMode = RoomNotificationMode.ALL_MESSAGES, + initialRoomModeIsDefault = false + ) + val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService).apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val roomListService = FakeRoomListService() + val presenter = createPresenter(notificationSettingsService, roomListService, matrixClient) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + roomListService.postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail(notificationMode = RoomNotificationMode.ALL_MESSAGES)))) + val loadedState = consumeItemsUntilPredicate { state -> + state.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES } + }.last() + Truth.assertThat(loadedState.roomsWithUserDefinedMode.any { it.details.notificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue() + } + } + @Test fun `present - edit default notification setting`() = runTest { - val notificationSettingsService = FakeNotificationSettingsService() - val presenter = EditDefaultNotificationSettingPresenter(notificationSettingsService = notificationSettingsService, isOneToOne = false) + val presenter = createPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -61,4 +89,18 @@ class EditDefaultNotificationSettingsPresenterTests { Truth.assertThat(loadedState.mode).isEqualTo(RoomNotificationMode.ALL_MESSAGES) } } + + private fun createPresenter( + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), + roomListService: FakeRoomListService = FakeRoomListService(), + matrixClient: FakeMatrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) + ): EditDefaultNotificationSettingPresenter { + return EditDefaultNotificationSettingPresenter( + notificationSettingsService = notificationSettingsService, + isOneToOne = false, + roomListService = roomListService, + matrixClient = matrixClient + ) + } + } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt index a1b7831ce2..7d4c83d518 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt @@ -24,14 +24,17 @@ import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsEvents import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsPresenter import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.test.runTest import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds class RoomNotificationSettingsPresenterTests { @Test fun `present - initial state is created from room info`() = runTest { - val presenter = aNotificationPresenter + val presenter = aNotificationPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -44,7 +47,7 @@ class RoomNotificationSettingsPresenterTests { @Test fun `present - notification mode changed`() = runTest { - val presenter = aNotificationPresenter + val presenter = aNotificationPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -56,27 +59,61 @@ class RoomNotificationSettingsPresenterTests { } } + @Test + fun `present - observe notification mode changed`() = runTest { + val notificationSettingsService = FakeNotificationSettingsService() + val presenter = aNotificationPresenter(notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + val updatedState = consumeItemsUntilPredicate() { + it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + }.last() + Truth.assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + } + } + + + @Test + fun `present - notification settings set custom`() = runTest { + val notificationSettingsService = FakeNotificationSettingsService() + val presenter = aNotificationPresenter(notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(false)) + val defaultState = consumeItemsUntilPredicate(timeout = 8000.milliseconds) { + it.roomNotificationSettings?.isDefault == false + }.last() + Truth.assertThat(defaultState.roomNotificationSettings?.isDefault).isFalse() + } + } + @Test fun `present - notification settings restore default`() = runTest { - val presenter = aNotificationPresenter + val presenter = aNotificationPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() initialState.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)) initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(true)) - val defaultState = consumeItemsUntilPredicate { + val defaultState = consumeItemsUntilPredicate(timeout = 2000.milliseconds) { it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }.last() Truth.assertThat(defaultState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) } } - private val aNotificationPresenter: RoomNotificationSettingsPresenter get() { - val room = aMatrixRoom() + private fun aNotificationPresenter( + notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() + ): RoomNotificationSettingsPresenter{ + val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) return RoomNotificationSettingsPresenter( room = room, - notificationSettingsService = room.notificationSettingsService + notificationSettingsService = notificationSettingsService ) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt index 77592d6d1f..2b65857053 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt @@ -20,12 +20,14 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings +import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NOTIFICATION_MODE import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow class FakeNotificationSettingsService( initialRoomMode: RoomNotificationMode = A_ROOM_NOTIFICATION_MODE, + initialRoomModeIsDefault: Boolean = true, initialGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, initialEncryptedGroupDefaultMode: RoomNotificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, initialOneToOneDefaultMode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES, @@ -37,6 +39,7 @@ class FakeNotificationSettingsService( private var defaultOneToOneRoomNotificationMode: RoomNotificationMode = initialOneToOneDefaultMode private var defaultEncryptedOneToOneRoomNotificationMode: RoomNotificationMode = initialEncryptedOneToOneDefaultMode private var roomNotificationMode: RoomNotificationMode = initialRoomMode + private var roomNotificationModeIsDefault: Boolean = initialRoomModeIsDefault private var callNotificationsEnabled = false private var atRoomNotificationsEnabled = false override val notificationSettingsChangeFlow: SharedFlow @@ -45,8 +48,8 @@ class FakeNotificationSettingsService( override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result { return Result.success( RoomNotificationSettings( - mode = roomNotificationMode, - isDefault = roomNotificationMode == defaultEncryptedGroupRoomNotificationMode + mode = if(roomNotificationModeIsDefault) defaultEncryptedGroupRoomNotificationMode else roomNotificationMode, + isDefault = roomNotificationModeIsDefault ) ) } @@ -86,12 +89,14 @@ class FakeNotificationSettingsService( } override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result { + roomNotificationModeIsDefault = false roomNotificationMode = mode _notificationSettingsStateFlow.emit(Unit) return Result.success(Unit) } override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result { + roomNotificationModeIsDefault = true roomNotificationMode = defaultEncryptedGroupRoomNotificationMode _notificationSettingsStateFlow.emit(Unit) return Result.success(Unit) @@ -122,4 +127,8 @@ class FakeNotificationSettingsService( callNotificationsEnabled = enabled return Result.success(Unit) } + + override suspend fun getRoomsWithUserDefinedRules(): Result> { + return Result.success(if (roomNotificationModeIsDefault) listOf() else listOf(A_ROOM_ID.value)) + } } From 493d67c8caeb2a20af8ee168562cc6760f8c732d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Oct 2023 18:28:07 +0200 Subject: [PATCH 048/281] Introduce CryptoService for simple cryptographic operations --- .../cryptography/api/CipherFactory.kt | 40 ------------- .../cryptography/api/CryptoService.kt | 23 ++++---- .../cryptography/api/EncryptionResult.kt | 56 +++++++++++++++++++ ...pherFactory.kt => DefaultCryptoService.kt} | 49 +++++++++------- 4 files changed, 96 insertions(+), 72 deletions(-) delete mode 100644 libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CipherFactory.kt rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt => libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CryptoService.kt (52%) create mode 100644 libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt rename libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/{KeyStoreCipherFactory.kt => DefaultCryptoService.kt} (66%) diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CipherFactory.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CipherFactory.kt deleted file mode 100644 index 0eec6e9fa9..0000000000 --- a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CipherFactory.kt +++ /dev/null @@ -1,40 +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.cryptography.api - -import javax.crypto.Cipher - -/** - * Factory to create [Cipher] instances for encryption and decryption. - * The implementation should use a secure way to store the keys. - */ -interface CipherFactory { - /** - * Create a [Cipher] instance for encryption. - * @param alias the alias of the key used for encryption. - * @return the [Cipher] instance. - */ - fun createEncryptionCipher(alias: String): Cipher - - /** - * Create a [Cipher] instance for decryption. - * @param alias the alias of the key used for encryption. - * @param initializationVector the initialization vector used for encryption. - * @return the [Cipher] instance. - */ - fun createDecryptionCipher(alias: String, initializationVector: ByteArray): Cipher -} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CryptoService.kt similarity index 52% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt rename to libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CryptoService.kt index 8e3f45ac07..a05e11da2a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationStateProvider.kt +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CryptoService.kt @@ -14,17 +14,18 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.libraries.cryptography.api -import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import javax.crypto.Cipher +import javax.crypto.SecretKey -open class PinAuthenticationStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aPinAuthenticationState(), - ) +/** + * Simple service to provide cryptographic operations. + */ +interface CryptoService { + fun getOrCreateSecretKey(alias: String): SecretKey + fun createEncryptionCipher(key: SecretKey): Cipher + fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher + fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult + fun decrypt(key: SecretKey, encryptionResult: EncryptionResult): ByteArray } - -fun aPinAuthenticationState() = PinAuthenticationState( - eventSink = {} -) diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt new file mode 100644 index 0000000000..10affcfdcb --- /dev/null +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt @@ -0,0 +1,56 @@ +/* + * 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.cryptography.api + +import android.util.Base64 +import java.nio.ByteBuffer + +/** + * Holds the result of an encryption operation. + */ +class EncryptionResult( + val encryptedByteArray: ByteArray, + val initializationVector: ByteArray +) { + fun toBase64(): String { + val initializationVectorSize = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(initializationVector.size).array() + val cipherTextWithIv: ByteArray = + ByteBuffer.allocate(Int.SIZE_BYTES + initializationVector.size + encryptedByteArray.size) + .put(initializationVectorSize) + .put(initializationVector) + .put(encryptedByteArray) + .array() + return Base64.encodeToString(cipherTextWithIv, Base64.NO_WRAP) + } + + companion object { + /** + * @param base64 the base64 representation of the encrypted data. + * @return the [EncryptionResult] from the base64 representation. + */ + fun fromBase64(base64: String): EncryptionResult { + val cipherTextWithIv = Base64.decode(base64, Base64.NO_WRAP) + val buffer = ByteBuffer.wrap(cipherTextWithIv) + val initializationVectorSize = buffer.int + val initializationVector = ByteArray(initializationVectorSize) + buffer.get(initializationVector) + val encryptedByteArray = ByteArray(buffer.remaining()) + buffer.get(encryptedByteArray) + return EncryptionResult(encryptedByteArray, initializationVector) + } + } +} diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreCipherFactory.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/DefaultCryptoService.kt similarity index 66% rename from libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreCipherFactory.kt rename to libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/DefaultCryptoService.kt index eb88b66239..dc4f771acf 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreCipherFactory.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/DefaultCryptoService.kt @@ -19,7 +19,8 @@ package io.element.android.libraries.cryptography.impl import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.cryptography.api.CipherFactory +import io.element.android.libraries.cryptography.api.CryptoService +import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.di.AppScope import java.security.KeyStore import javax.crypto.Cipher @@ -34,28 +35,10 @@ private const val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE private const val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES private const val ENCRYPTION_AES_TRANSFORMATION = "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING" -/** - * Implementation of [CipherFactory] that uses the Android Keystore to store the keys. - */ @ContributesBinding(AppScope::class) -class KeyStoreCipherFactory @Inject constructor() : CipherFactory { +class DefaultCryptoService @Inject constructor() : CryptoService { - override fun createEncryptionCipher(alias: String): Cipher { - val cipher = Cipher.getInstance(ENCRYPTION_AES_TRANSFORMATION) - val secretKey = getOrGenerateKeyForAlias(alias) - cipher.init(Cipher.ENCRYPT_MODE, secretKey) - return cipher - } - - override fun createDecryptionCipher(alias: String, initializationVector: ByteArray): Cipher { - val cipher = Cipher.getInstance(ENCRYPTION_AES_TRANSFORMATION) - val secretKey = getOrGenerateKeyForAlias(alias) - val spec = GCMParameterSpec(128, initializationVector) - cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) - return cipher - } - - private fun getOrGenerateKeyForAlias(alias: String): SecretKey { + override fun getOrCreateSecretKey(alias: String): SecretKey { val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) ?.secretKey @@ -73,4 +56,28 @@ class KeyStoreCipherFactory @Inject constructor() : CipherFactory { generator.generateKey() } else secretKeyEntry } + + override fun createEncryptionCipher(key: SecretKey): Cipher { + return Cipher.getInstance(ENCRYPTION_AES_TRANSFORMATION).apply { + init(Cipher.ENCRYPT_MODE, key) + } + } + + override fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher { + val spec = GCMParameterSpec(128, initializationVector) + return Cipher.getInstance(ENCRYPTION_AES_TRANSFORMATION).apply { + init(Cipher.DECRYPT_MODE, key, spec) + } + } + + override fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult { + val cipher = createEncryptionCipher(key) + val encryptedData = cipher.doFinal(input) + return EncryptionResult(encryptedData, cipher.iv) + } + + override fun decrypt(key: SecretKey, encryptionResult: EncryptionResult): ByteArray { + val cipher = createDecryptionCipher(key, encryptionResult.initializationVector) + return cipher.doFinal(encryptionResult.encryptedByteArray) + } } From 981430731d36b70fdc4c0d40e25f579011bdc45d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Oct 2023 18:28:39 +0200 Subject: [PATCH 049/281] Pin : move some classes around and introduce PinCodeManager --- features/pin/impl/build.gradle.kts | 1 + .../pin/impl/pin/DefaultPinCodeManager.kt | 70 +++++++++++++++++++ .../features/pin/impl/pin/PinCodeManager.kt | 59 ++++++++++++++++ .../storage/EncryptedPinCodeStorage.kt | 8 +-- .../impl/{ => pin}/storage/PinCodeStore.kt | 2 +- .../storage/SharedPreferencesPinCodeStore.kt | 21 +++--- .../DefaultPinEntryPoint.kt | 2 +- .../impl/{ => presentation}/PinFlowNode.kt | 6 +- .../auth/PinAuthenticationEvents.kt | 2 +- .../auth/PinAuthenticationNode.kt | 2 +- .../auth/PinAuthenticationPresenter.kt | 2 +- .../auth/PinAuthenticationState.kt | 2 +- .../auth/PinAuthenticationStateProvider.kt | 30 ++++++++ .../auth/PinAuthenticationView.kt | 2 +- .../create/CreatePinEvents.kt | 2 +- .../create/CreatePinNode.kt | 2 +- .../create/CreatePinPresenter.kt | 2 +- .../create/CreatePinState.kt | 2 +- .../create/CreatePinStateProvider.kt | 2 +- .../create/CreatePinView.kt | 2 +- 20 files changed, 188 insertions(+), 33 deletions(-) create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/DefaultPinCodeManager.kt create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/PinCodeManager.kt rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => pin}/storage/EncryptedPinCodeStorage.kt (85%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => pin}/storage/PinCodeStore.kt (96%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => pin}/storage/SharedPreferencesPinCodeStore.kt (80%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/DefaultPinEntryPoint.kt (95%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/PinFlowNode.kt (91%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/auth/PinAuthenticationEvents.kt (91%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/auth/PinAuthenticationNode.kt (95%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/auth/PinAuthenticationPresenter.kt (95%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/auth/PinAuthenticationState.kt (91%) create mode 100644 features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationStateProvider.kt rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/auth/PinAuthenticationView.kt (97%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/create/CreatePinEvents.kt (91%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/create/CreatePinNode.kt (95%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/create/CreatePinPresenter.kt (94%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/create/CreatePinState.kt (91%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/create/CreatePinStateProvider.kt (93%) rename features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/{ => presentation}/create/CreatePinView.kt (96%) diff --git a/features/pin/impl/build.gradle.kts b/features/pin/impl/build.gradle.kts index 6cfc9fce11..e734035ab3 100644 --- a/features/pin/impl/build.gradle.kts +++ b/features/pin/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) + implementation(projects.libraries.cryptography.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/DefaultPinCodeManager.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/DefaultPinCodeManager.kt new file mode 100644 index 0000000000..4ee1a530a2 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/DefaultPinCodeManager.kt @@ -0,0 +1,70 @@ +/* + * 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.pin.impl.pin + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.pin.impl.pin.storage.PinCodeStore +import io.element.android.libraries.cryptography.api.CryptoService +import io.element.android.libraries.cryptography.api.EncryptionResult +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +private const val SECRET_KEY_ALIAS = "SECRET_KEY_ALIAS_PIN_CODE" + +@ContributesBinding(AppScope::class) +class DefaultPinCodeManager @Inject constructor( + private val cryptoService: CryptoService, + private val pinCodeStore: PinCodeStore, +) : PinCodeManager { + + override suspend fun isPinCodeAvailable(): Boolean { + return pinCodeStore.hasPinCode() + } + + override suspend fun createPinCode(pinCode: String) { + val secretKey = cryptoService.getOrCreateSecretKey(SECRET_KEY_ALIAS) + val encryptedPinCode = cryptoService.encrypt(secretKey, pinCode.toByteArray()).toBase64() + pinCodeStore.saveEncryptedPinCode(encryptedPinCode) + } + + override suspend fun verifyPinCode(pinCode: String): Boolean { + val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return false + return try { + val secretKey = cryptoService.getOrCreateSecretKey(SECRET_KEY_ALIAS) + val decryptedPinCode = cryptoService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) + decryptedPinCode.contentEquals(pinCode.toByteArray()) + } catch (failure: Throwable) { + false + } + } + + override suspend fun deletePinCode() { + pinCodeStore.deleteEncryptedPinCode() + } + + override suspend fun getRemainingPinCodeAttemptsNumber(): Int { + return pinCodeStore.getRemainingPinCodeAttemptsNumber() + } + + override suspend fun onWrongPin(): Int { + return pinCodeStore.onWrongPin() + } + + override suspend fun resetCounter() { + pinCodeStore.resetCounter() + } +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/PinCodeManager.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/PinCodeManager.kt new file mode 100644 index 0000000000..df4262d9eb --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/PinCodeManager.kt @@ -0,0 +1,59 @@ +/* + * 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.pin.impl.pin + +/** + * This interface is the main interface to manage the pin code. + * Implementation should take care of encrypting the pin code and storing it. + */ +interface PinCodeManager { + /** + * @return true if a pin code is available. + */ + suspend fun isPinCodeAvailable(): Boolean + + /** + * creates a new encrypted pin code. + * @param pinCode the clear pin code to create + */ + suspend fun createPinCode(pinCode: String) + + /** + * @return true if the pin code is correct + */ + suspend fun verifyPinCode(pinCode: String): Boolean + + /** + * deletes the previously created pin code + */ + suspend fun deletePinCode() + + /** + * @return the number of remaining attempts before the pin code is blocked + */ + suspend fun getRemainingPinCodeAttemptsNumber(): Int + + /** + * @return the number of remaining attempts before the pin code is blocked + */ + suspend fun onWrongPin(): Int + + /** + * Resets the counter of attempts for PIN code. + */ + suspend fun resetCounter() +} diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/EncryptedPinCodeStorage.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/EncryptedPinCodeStorage.kt similarity index 85% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/EncryptedPinCodeStorage.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/EncryptedPinCodeStorage.kt index ae3bd7b893..bf1dec8d0d 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/EncryptedPinCodeStorage.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/EncryptedPinCodeStorage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.storage +package io.element.android.features.pin.impl.pin.storage /** * Should be implemented by any class that provides access to the encrypted PIN code. @@ -24,17 +24,17 @@ interface EncryptedPinCodeStorage { /** * Returns the encrypted PIN code. */ - suspend fun getPinCode(): String? + suspend fun getEncryptedCode(): String? /** * Saves the encrypted PIN code to some persistable storage. */ - suspend fun savePinCode(pinCode: String) + suspend fun saveEncryptedPinCode(pinCode: String) /** * Deletes the PIN code from some persistable storage. */ - suspend fun deletePinCode() + suspend fun deleteEncryptedPinCode() /** * Returns whether the PIN code is stored or not. diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/PinCodeStore.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/PinCodeStore.kt similarity index 96% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/PinCodeStore.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/PinCodeStore.kt index 5c54cc26f9..3476890275 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/PinCodeStore.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/PinCodeStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.storage +package io.element.android.features.pin.impl.pin.storage interface PinCodeStore : EncryptedPinCodeStorage { diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/SharedPreferencesPinCodeStore.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/SharedPreferencesPinCodeStore.kt similarity index 80% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/SharedPreferencesPinCodeStore.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/SharedPreferencesPinCodeStore.kt index fc8155352f..892336f01d 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/storage/SharedPreferencesPinCodeStore.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/SharedPreferencesPinCodeStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.storage +package io.element.android.features.pin.impl.pin.storage import android.content.SharedPreferences import androidx.core.content.edit @@ -26,6 +26,10 @@ import kotlinx.coroutines.withContext import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject +private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY" +private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY" +private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 + @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class SharedPreferencesPinCodeStore @Inject constructor( @@ -35,11 +39,11 @@ class SharedPreferencesPinCodeStore @Inject constructor( private val listeners = CopyOnWriteArrayList() - override suspend fun getPinCode(): String? = withContext(dispatchers.io) { + override suspend fun getEncryptedCode(): String? = withContext(dispatchers.io) { sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null) } - override suspend fun savePinCode(pinCode: String) = withContext(dispatchers.io) { + override suspend fun saveEncryptedPinCode(pinCode: String) = withContext(dispatchers.io) { sharedPreferences.edit { putString(ENCODED_PIN_CODE_KEY, pinCode) } @@ -48,7 +52,7 @@ class SharedPreferencesPinCodeStore @Inject constructor( } } - override suspend fun deletePinCode() = withContext(dispatchers.io) { + override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) { // Also reset the counters resetCounter() sharedPreferences.edit { @@ -78,7 +82,6 @@ class SharedPreferencesPinCodeStore @Inject constructor( override suspend fun resetCounter() = withContext(dispatchers.io) { sharedPreferences.edit { remove(REMAINING_PIN_CODE_ATTEMPTS_KEY) - remove(REMAINING_BIOMETRICS_ATTEMPTS_KEY) } } @@ -89,12 +92,4 @@ class SharedPreferencesPinCodeStore @Inject constructor( override fun removeListener(listener: PinCodeStore.Listener) { listeners.remove(listener) } - - companion object { - private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY" - private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY" - private const val REMAINING_BIOMETRICS_ATTEMPTS_KEY = "REMAINING_BIOMETRICS_ATTEMPTS_KEY" - - private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 - } } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/DefaultPinEntryPoint.kt similarity index 95% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/DefaultPinEntryPoint.kt index 920691cad2..17b7f6d612 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/DefaultPinEntryPoint.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/DefaultPinEntryPoint.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl +package io.element.android.features.pin.impl.presentation import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/PinFlowNode.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/PinFlowNode.kt index a76504ce8a..acf10ffcce 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/PinFlowNode.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/PinFlowNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl +package io.element.android.features.pin.impl.presentation import android.os.Parcelable import androidx.compose.runtime.Composable @@ -27,8 +27,8 @@ import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.pin.impl.auth.PinAuthenticationNode -import io.element.android.features.pin.impl.create.CreatePinNode +import io.element.android.features.pin.impl.presentation.auth.PinAuthenticationNode +import io.element.android.features.pin.impl.presentation.create.CreatePinNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationEvents.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationEvents.kt index 110c62660a..01835df18c 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationEvents.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.pin.impl.presentation.auth sealed interface PinAuthenticationEvents { data object Unlock : PinAuthenticationEvents diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationNode.kt similarity index 95% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationNode.kt index b5dab44c96..ec4b78dc15 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationNode.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.pin.impl.presentation.auth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationPresenter.kt similarity index 95% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationPresenter.kt index 5e7e274ba7..bb1e5d04ad 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationPresenter.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.pin.impl.presentation.auth import androidx.compose.runtime.Composable import io.element.android.features.pin.api.PinStateService diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationState.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationState.kt index 2df1e50f83..3d319356ec 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationState.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.pin.impl.presentation.auth data class PinAuthenticationState( val eventSink: (PinAuthenticationEvents) -> Unit diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationStateProvider.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationStateProvider.kt new file mode 100644 index 0000000000..3cfbe5cb84 --- /dev/null +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationStateProvider.kt @@ -0,0 +1,30 @@ +/* + * 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.pin.impl.presentation.auth + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class PinAuthenticationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPinAuthenticationState(), + ) +} + +fun aPinAuthenticationState() = PinAuthenticationState( + eventSink = {} +) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationView.kt similarity index 97% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationView.kt index 9fe689bb39..9efab19813 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/auth/PinAuthenticationView.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.auth +package io.element.android.features.pin.impl.presentation.auth import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinEvents.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinEvents.kt index 280856b5c8..87aee27557 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinEvents.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.pin.impl.presentation.create sealed interface CreatePinEvents { object MyEvent : CreatePinEvents diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinNode.kt similarity index 95% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinNode.kt index 0ed0343a5b..6b71510d5a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinNode.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.pin.impl.presentation.create import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinPresenter.kt similarity index 94% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinPresenter.kt index d45257b4bd..8cfdc22e46 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinPresenter.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.pin.impl.presentation.create import androidx.compose.runtime.Composable import io.element.android.libraries.architecture.Presenter diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinState.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinState.kt index c405db82ec..64c0ae0a5a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinState.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.pin.impl.presentation.create data class CreatePinState( val eventSink: (CreatePinEvents) -> Unit diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinStateProvider.kt similarity index 93% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinStateProvider.kt index 4bff72023e..0afe3442a2 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinStateProvider.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.pin.impl.presentation.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinView.kt similarity index 96% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt rename to features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinView.kt index 64e5be4091..643f487d1b 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/create/CreatePinView.kt +++ b/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.create +package io.element.android.features.pin.impl.presentation.create import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme From 84368065715719b926856527c763dca1adcbd71b Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Oct 2023 18:38:30 +0200 Subject: [PATCH 050/281] Pin: rename feature pin to lockscreen --- .../android/appnav/LoggedInFlowNode.kt | 24 +++++++++---------- .../{pin => lockscreen}/api/build.gradle.kts | 2 +- .../lockscreen/api/LockScreenEntryPoint.kt} | 4 ++-- .../lockscreen/api/LockScreenState.kt} | 8 +++---- .../lockscreen/api/LockScreenStateService.kt} | 6 ++--- .../{pin => lockscreen}/impl/build.gradle.kts | 4 ++-- .../impl/DefaultLockScreenEntryPoint.kt} | 8 +++---- .../lockscreen/impl/LockScreenFlowNode.kt} | 10 ++++---- .../impl}/auth/PinAuthenticationEvents.kt | 2 +- .../impl}/auth/PinAuthenticationNode.kt | 2 +- .../impl}/auth/PinAuthenticationPresenter.kt | 6 ++--- .../impl}/auth/PinAuthenticationState.kt | 2 +- .../auth/PinAuthenticationStateProvider.kt | 2 +- .../impl}/auth/PinAuthenticationView.kt | 2 +- .../impl}/create/CreatePinEvents.kt | 2 +- .../lockscreen/impl}/create/CreatePinNode.kt | 2 +- .../impl}/create/CreatePinPresenter.kt | 2 +- .../lockscreen/impl}/create/CreatePinState.kt | 2 +- .../impl}/create/CreatePinStateProvider.kt | 2 +- .../lockscreen/impl}/create/CreatePinView.kt | 2 +- .../impl/pin/DefaultPinCodeManager.kt | 4 ++-- .../lockscreen}/impl/pin/PinCodeManager.kt | 2 +- .../pin/storage/EncryptedPinCodeStorage.kt | 2 +- .../impl/pin/storage/PinCodeStore.kt | 2 +- .../storage/SharedPreferencesPinCodeStore.kt | 2 +- .../state/DefaultLockScreenStateService.kt} | 18 +++++++------- 26 files changed, 62 insertions(+), 62 deletions(-) rename features/{pin => lockscreen}/api/build.gradle.kts (92%) rename features/{pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt => lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt} (86%) rename features/{pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt => lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt} (78%) rename features/{pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt => lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt} (85%) rename features/{pin => lockscreen}/impl/build.gradle.kts (93%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/DefaultPinEntryPoint.kt => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt} (78%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/PinFlowNode.kt => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt} (88%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/auth/PinAuthenticationEvents.kt (91%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/auth/PinAuthenticationNode.kt (95%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/auth/PinAuthenticationPresenter.kt (87%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/auth/PinAuthenticationState.kt (91%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/auth/PinAuthenticationStateProvider.kt (93%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/auth/PinAuthenticationView.kt (97%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/create/CreatePinEvents.kt (91%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/create/CreatePinNode.kt (95%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/create/CreatePinPresenter.kt (94%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/create/CreatePinState.kt (91%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/create/CreatePinStateProvider.kt (93%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl}/create/CreatePinView.kt (96%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen}/impl/pin/DefaultPinCodeManager.kt (94%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen}/impl/pin/PinCodeManager.kt (96%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen}/impl/pin/storage/EncryptedPinCodeStorage.kt (95%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen}/impl/pin/storage/PinCodeStore.kt (95%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen}/impl/pin/storage/SharedPreferencesPinCodeStore.kt (98%) rename features/{pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt => lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt} (75%) 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 82786b1a0c..2f7eabb582 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -50,9 +50,9 @@ import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.invitelist.api.InviteListEntryPoint import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus -import io.element.android.features.pin.api.PinEntryPoint -import io.element.android.features.pin.api.PinState -import io.element.android.features.pin.api.PinStateService +import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.features.lockscreen.api.LockScreenState +import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -93,8 +93,8 @@ class LoggedInFlowNode @AssistedInject constructor( private val networkMonitor: NetworkMonitor, private val notificationDrawerManager: NotificationDrawerManager, private val ftueState: FtueState, - private val pinEntryPoint: PinEntryPoint, - private val pinStateService: PinStateService, + private val lockScreenEntryPoint: LockScreenEntryPoint, + private val lockScreenStateService: LockScreenStateService, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( @@ -136,12 +136,12 @@ class LoggedInFlowNode @AssistedInject constructor( }, onResume = { coroutineScope.launch { - pinStateService.entersForeground() + lockScreenStateService.entersForeground() } }, onPause = { coroutineScope.launch { - pinStateService.entersBackground() + lockScreenStateService.entersBackground() } }, onStop = { @@ -218,7 +218,7 @@ class LoggedInFlowNode @AssistedInject constructor( createNode(buildContext) } NavTarget.LockPermanent -> { - pinEntryPoint.createNode(this, buildContext) + lockScreenEntryPoint.createNode(this, buildContext) } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { @@ -345,9 +345,9 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { - val pinState by pinStateService.pinState.collectAsState() - when (pinState) { - PinState.Unlocked -> { + val lockScreenState by lockScreenStateService.state.collectAsState() + when (lockScreenState) { + LockScreenState.Unlocked -> { Children( navModel = backstack, modifier = Modifier, @@ -359,7 +359,7 @@ class LoggedInFlowNode @AssistedInject constructor( PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) } } - PinState.Locked -> { + LockScreenState.Locked -> { MoveActivityToBackgroundBackHandler() PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent) } diff --git a/features/pin/api/build.gradle.kts b/features/lockscreen/api/build.gradle.kts similarity index 92% rename from features/pin/api/build.gradle.kts rename to features/lockscreen/api/build.gradle.kts index 95b062b0c8..97f472517c 100644 --- a/features/pin/api/build.gradle.kts +++ b/features/lockscreen/api/build.gradle.kts @@ -19,7 +19,7 @@ plugins { } android { - namespace = "io.element.android.features.pin.api" + namespace = "io.element.android.features.lockscreen.api" } dependencies { diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt similarity index 86% rename from features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt rename to features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt index 1fe3caf574..3c9aceb2c8 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinEntryPoint.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt @@ -14,8 +14,8 @@ * limitations under the License. */ -package io.element.android.features.pin.api +package io.element.android.features.lockscreen.api import io.element.android.libraries.architecture.SimpleFeatureEntryPoint -interface PinEntryPoint : SimpleFeatureEntryPoint +interface LockScreenEntryPoint : SimpleFeatureEntryPoint diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt similarity index 78% rename from features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt rename to features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt index 0ff1b0b3d5..d1e53cfdcc 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinState.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.pin.api +package io.element.android.features.lockscreen.api -sealed interface PinState { - data object Unlocked : PinState - data object Locked : PinState +sealed interface LockScreenState { + data object Unlocked : LockScreenState + data object Locked : LockScreenState } diff --git a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt similarity index 85% rename from features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt rename to features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt index 4ecb473c18..2f2e6b2376 100644 --- a/features/pin/api/src/main/kotlin/io/element/android/features/pin/api/PinStateService.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.element.android.features.pin.api +package io.element.android.features.lockscreen.api import kotlinx.coroutines.flow.StateFlow -interface PinStateService { - val pinState: StateFlow +interface LockScreenStateService { + val state: StateFlow suspend fun entersForeground() suspend fun entersBackground() diff --git a/features/pin/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts similarity index 93% rename from features/pin/impl/build.gradle.kts rename to features/lockscreen/impl/build.gradle.kts index e734035ab3..51e766bd36 100644 --- a/features/pin/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -22,7 +22,7 @@ plugins { } android { - namespace = "io.element.android.features.pin.impl" + namespace = "io.element.android.features.lockscreen.impl" } anvil { @@ -32,7 +32,7 @@ anvil { dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) - api(projects.features.pin.api) + api(projects.features.lockscreen.api) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/DefaultPinEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt similarity index 78% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/DefaultPinEntryPoint.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 17b7f6d612..736be374cd 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/DefaultPinEntryPoint.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt @@ -14,20 +14,20 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation +package io.element.android.features.lockscreen.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.pin.api.PinEntryPoint +import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPinEntryPoint @Inject constructor() : PinEntryPoint { +class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + return parentNode.createNode(buildContext) } } diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/PinFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt similarity index 88% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/PinFlowNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index acf10ffcce..d2989d53cc 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/PinFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation +package io.element.android.features.lockscreen.impl import android.os.Parcelable import androidx.compose.runtime.Composable @@ -27,8 +27,8 @@ import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.pin.impl.presentation.auth.PinAuthenticationNode -import io.element.android.features.pin.impl.presentation.create.CreatePinNode +import io.element.android.features.lockscreen.impl.auth.PinAuthenticationNode +import io.element.android.features.lockscreen.impl.create.CreatePinNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -36,10 +36,10 @@ import io.element.android.libraries.di.AppScope import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) -class PinFlowNode @AssistedInject constructor( +class LockScreenFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, -) : BackstackNode( +) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Auth, savedStateMap = buildContext.savedStateMap, diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt index 01835df18c..f9f46c430a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.auth +package io.element.android.features.lockscreen.impl.auth sealed interface PinAuthenticationEvents { data object Unlock : PinAuthenticationEvents diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt similarity index 95% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt index ec4b78dc15..d236d40cf1 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.auth +package io.element.android.features.lockscreen.impl.auth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt similarity index 87% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt index bb1e5d04ad..ecc82f421c 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt @@ -14,17 +14,17 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.auth +package io.element.android.features.lockscreen.impl.auth import androidx.compose.runtime.Composable -import io.element.android.features.pin.api.PinStateService +import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class PinAuthenticationPresenter @Inject constructor( - private val pinStateService: PinStateService, + private val pinStateService: LockScreenStateService, private val coroutineScope: CoroutineScope, ) : Presenter { diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt index 3d319356ec..387467534f 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.auth +package io.element.android.features.lockscreen.impl.auth data class PinAuthenticationState( val eventSink: (PinAuthenticationEvents) -> Unit diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt similarity index 93% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt index 3cfbe5cb84..a2612ed858 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.auth +package io.element.android.features.lockscreen.impl.auth import androidx.compose.ui.tooling.preview.PreviewParameterProvider diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt similarity index 97% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt index 9efab19813..2b62e46800 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/auth/PinAuthenticationView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.auth +package io.element.android.features.lockscreen.impl.auth import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index 87aee27557..deb3095e69 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.create +package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { object MyEvent : CreatePinEvents diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt similarity index 95% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt index 6b71510d5a..3689c0cc76 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.create +package io.element.android.features.lockscreen.impl.create import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt similarity index 94% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 8cfdc22e46..08ba24e074 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.create +package io.element.android.features.lockscreen.impl.create import androidx.compose.runtime.Composable import io.element.android.libraries.architecture.Presenter diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt similarity index 91% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 64c0ae0a5a..67311639ad 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.create +package io.element.android.features.lockscreen.impl.create data class CreatePinState( val eventSink: (CreatePinEvents) -> Unit diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt similarity index 93% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index 0afe3442a2..a918b5193e 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.create +package io.element.android.features.lockscreen.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt similarity index 96% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 643f487d1b..120c0b6079 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/presentation/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.presentation.create +package io.element.android.features.lockscreen.impl.create import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt similarity index 94% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/DefaultPinCodeManager.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index 4ee1a530a2..e7e27bf5f3 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.pin +package io.element.android.features.lockscreen.impl.pin import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.pin.impl.pin.storage.PinCodeStore +import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore import io.element.android.libraries.cryptography.api.CryptoService import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.di.AppScope diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt similarity index 96% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/PinCodeManager.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index df4262d9eb..960b6ecba1 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.pin +package io.element.android.features.lockscreen.impl.pin /** * This interface is the main interface to manage the pin code. diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/EncryptedPinCodeStorage.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt similarity index 95% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/EncryptedPinCodeStorage.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt index bf1dec8d0d..2345eaf481 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/EncryptedPinCodeStorage.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/EncryptedPinCodeStorage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.pin.storage +package io.element.android.features.lockscreen.impl.pin.storage /** * Should be implemented by any class that provides access to the encrypted PIN code. diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/PinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt similarity index 95% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/PinCodeStore.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt index 3476890275..e72cbca2db 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/PinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.pin.storage +package io.element.android.features.lockscreen.impl.pin.storage interface PinCodeStore : EncryptedPinCodeStorage { diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt similarity index 98% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/SharedPreferencesPinCodeStore.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt index 892336f01d..dbeabb53ed 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.pin.storage +package io.element.android.features.lockscreen.impl.pin.storage import android.content.SharedPreferences import androidx.core.content.edit diff --git a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt similarity index 75% rename from features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt index 9accef2b80..dbfeca2c6a 100644 --- a/features/pin/impl/src/main/kotlin/io/element/android/features/pin/impl/state/DefaultPinStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.pin.impl.state +package io.element.android.features.lockscreen.impl.state import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.pin.api.PinState -import io.element.android.features.pin.api.PinStateService +import io.element.android.features.lockscreen.api.LockScreenState +import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -35,18 +35,18 @@ private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class DefaultPinStateService @Inject constructor( +class DefaultLockScreenStateService @Inject constructor( private val featureFlagService: FeatureFlagService, -) : PinStateService { +) : LockScreenStateService { - private val _pinState = MutableStateFlow(PinState.Unlocked) - override val pinState: StateFlow = _pinState + private val _lockScreenState = MutableStateFlow(LockScreenState.Unlocked) + override val state: StateFlow = _lockScreenState private var lockJob: Job? = null override suspend fun unlock() { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - _pinState.value = PinState.Unlocked + _lockScreenState.value = LockScreenState.Unlocked } } @@ -58,7 +58,7 @@ class DefaultPinStateService @Inject constructor( lockJob = launch { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { delay(GRACE_PERIOD_IN_MILLIS) - _pinState.value = PinState.Locked + _lockScreenState.value = LockScreenState.Locked } } } From d6d553e8e0650a74341edb333ce05390eb1bc7ae Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 11:26:02 +0200 Subject: [PATCH 051/281] Pin code: add some tests --- features/lockscreen/impl/build.gradle.kts | 2 + .../impl/pin/DefaultPinCodeManager.kt | 14 +++-- .../impl/pin/DefaultPinCodeManagerTest.kt | 53 +++++++++++++++++ .../impl/pin/storage/InMemoryPinCodeStore.kt | 59 +++++++++++++++++++ .../cryptography/api/AESEncryptionSpecs.kt | 27 +++++++++ ...vice.kt => EncryptionDecryptionService.kt} | 5 +- .../cryptography/api/EncryptionResult.kt | 9 ++- .../cryptography/api/SecretKeyProvider.kt | 27 +++++++++ libraries/cryptography/impl/build.gradle.kts | 4 ++ ...e.kt => AESEncryptionDecryptionService.kt} | 41 +++---------- .../impl/KeyStoreSecretKeyProvider.kt | 55 +++++++++++++++++ .../AESEncryptionDecryptionServiceTest.kt | 54 +++++++++++++++++ libraries/cryptography/test/build.gradle.kts | 27 +++++++++ .../test/SimpleSecretKeyProvider.kt | 39 ++++++++++++ 14 files changed, 371 insertions(+), 45 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt create mode 100644 libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt rename libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/{CryptoService.kt => EncryptionDecryptionService.kt} (88%) create mode 100644 libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt rename libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/{DefaultCryptoService.kt => AESEncryptionDecryptionService.kt} (52%) create mode 100644 libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt create mode 100644 libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt create mode 100644 libraries/cryptography/test/build.gradle.kts create mode 100644 libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 51e766bd36..af63538db5 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -47,6 +47,8 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.cryptography.test) + testImplementation(projects.libraries.cryptography.impl) ksp(libs.showkase.processor) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index e7e27bf5f3..e7529e9280 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -18,8 +18,9 @@ package io.element.android.features.lockscreen.impl.pin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore -import io.element.android.libraries.cryptography.api.CryptoService +import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.api.EncryptionResult +import io.element.android.libraries.cryptography.api.SecretKeyProvider import io.element.android.libraries.di.AppScope import javax.inject.Inject @@ -27,7 +28,8 @@ private const val SECRET_KEY_ALIAS = "SECRET_KEY_ALIAS_PIN_CODE" @ContributesBinding(AppScope::class) class DefaultPinCodeManager @Inject constructor( - private val cryptoService: CryptoService, + private val secretKeyProvider: SecretKeyProvider, + private val encryptionDecryptionService: EncryptionDecryptionService, private val pinCodeStore: PinCodeStore, ) : PinCodeManager { @@ -36,16 +38,16 @@ class DefaultPinCodeManager @Inject constructor( } override suspend fun createPinCode(pinCode: String) { - val secretKey = cryptoService.getOrCreateSecretKey(SECRET_KEY_ALIAS) - val encryptedPinCode = cryptoService.encrypt(secretKey, pinCode.toByteArray()).toBase64() + val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) + val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) } override suspend fun verifyPinCode(pinCode: String): Boolean { val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return false return try { - val secretKey = cryptoService.getOrCreateSecretKey(SECRET_KEY_ALIAS) - val decryptedPinCode = cryptoService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) + val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) + val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) decryptedPinCode.contentEquals(pinCode.toByteArray()) } catch (failure: Throwable) { false diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt new file mode 100644 index 0000000000..a3f29c5351 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt @@ -0,0 +1,53 @@ +/* + * 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.lockscreen.impl.pin + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore +import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService +import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultPinCodeManagerTest { + + private val pinCodeStore = InMemoryPinCodeStore() + private val secretKeyProvider = SimpleSecretKeyProvider() + private val encryptionDecryptionService = AESEncryptionDecryptionService() + private val pinCodeManager = DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) + + @Test + fun given_a_pin_code_when_create_and_delete_assert_no_pin_code_left() = runTest { + pinCodeManager.createPinCode("1234") + assertThat(pinCodeManager.isPinCodeAvailable()).isTrue() + pinCodeManager.deletePinCode() + assertThat(pinCodeManager.isPinCodeAvailable()).isFalse() + } + + @Test + fun given_a_pin_code_when_create_and_verify_with_the_same_pin_succeed() = runTest { + val pinCode = "1234" + pinCodeManager.createPinCode(pinCode) + assertThat(pinCodeManager.verifyPinCode(pinCode)).isTrue() + } + + @Test + fun given_a_pin_code_when_create_and_verify_with_a_different_pin_fails() = runTest { + pinCodeManager.createPinCode("1234") + assertThat(pinCodeManager.verifyPinCode("1235")).isFalse() + } +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt new file mode 100644 index 0000000000..ed949c08de --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt @@ -0,0 +1,59 @@ +/* + * 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.lockscreen.impl.pin.storage + +class InMemoryPinCodeStore : PinCodeStore { + + private var pinCode: String? = null + private var remainingAttempts: Int = 3 + + override suspend fun getRemainingPinCodeAttemptsNumber(): Int { + return remainingAttempts + } + + override suspend fun onWrongPin(): Int { + return remainingAttempts-- + } + + override suspend fun resetCounter() { + remainingAttempts = 3 + } + + override fun addListener(listener: PinCodeStore.Listener) { + // no-op + } + + override fun removeListener(listener: PinCodeStore.Listener) { + // no-op + } + + override suspend fun getEncryptedCode(): String? { + return pinCode + } + + override suspend fun saveEncryptedPinCode(pinCode: String) { + this.pinCode = pinCode + } + + override suspend fun deleteEncryptedPinCode() { + pinCode = null + } + + override suspend fun hasPinCode(): Boolean { + return pinCode != null + } +} diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt new file mode 100644 index 0000000000..d4be4a1f4f --- /dev/null +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/AESEncryptionSpecs.kt @@ -0,0 +1,27 @@ +/* + * 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.cryptography.api + +import android.security.keystore.KeyProperties + +object AESEncryptionSpecs { + const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM + const val PADDINGS = KeyProperties.ENCRYPTION_PADDING_NONE + const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + const val KEY_SIZE = 128 + const val CIPHER_TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDINGS" +} diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CryptoService.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt similarity index 88% rename from libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CryptoService.kt rename to libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt index a05e11da2a..d670f7b1d2 100644 --- a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/CryptoService.kt +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt @@ -20,10 +20,9 @@ import javax.crypto.Cipher import javax.crypto.SecretKey /** - * Simple service to provide cryptographic operations. + * Simple service to provide encryption and decryption operations. */ -interface CryptoService { - fun getOrCreateSecretKey(alias: String): SecretKey +interface EncryptionDecryptionService { fun createEncryptionCipher(key: SecretKey): Cipher fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt index 10affcfdcb..5aa3a0cbea 100644 --- a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionResult.kt @@ -14,10 +14,13 @@ * limitations under the License. */ +@file:OptIn(ExperimentalEncodingApi::class) + package io.element.android.libraries.cryptography.api -import android.util.Base64 import java.nio.ByteBuffer +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi /** * Holds the result of an encryption operation. @@ -34,7 +37,7 @@ class EncryptionResult( .put(initializationVector) .put(encryptedByteArray) .array() - return Base64.encodeToString(cipherTextWithIv, Base64.NO_WRAP) + return Base64.encode(cipherTextWithIv) } companion object { @@ -43,7 +46,7 @@ class EncryptionResult( * @return the [EncryptionResult] from the base64 representation. */ fun fromBase64(base64: String): EncryptionResult { - val cipherTextWithIv = Base64.decode(base64, Base64.NO_WRAP) + val cipherTextWithIv = Base64.decode(base64) val buffer = ByteBuffer.wrap(cipherTextWithIv) val initializationVectorSize = buffer.int val initializationVector = ByteArray(initializationVectorSize) diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt new file mode 100644 index 0000000000..85f57ac07f --- /dev/null +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt @@ -0,0 +1,27 @@ +/* + * 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.cryptography.api + +import javax.crypto.SecretKey + +/** + * Simple interface to get or create a secret key for a given alias. + * Implementation should be able to store the generated key securely. + */ +interface SecretKeyProvider { + fun getOrCreateKey(alias: String): SecretKey +} diff --git a/libraries/cryptography/impl/build.gradle.kts b/libraries/cryptography/impl/build.gradle.kts index fa6f9db7ea..263fecec27 100644 --- a/libraries/cryptography/impl/build.gradle.kts +++ b/libraries/cryptography/impl/build.gradle.kts @@ -29,7 +29,11 @@ anvil { dependencies { anvil(projects.anvilcodegen) + implementation(libs.dagger) implementation(projects.anvilannotations) implementation(projects.libraries.di) implementation(projects.libraries.cryptography.api) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) } diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/DefaultCryptoService.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt similarity index 52% rename from libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/DefaultCryptoService.kt rename to libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt index dc4f771acf..cf1ea93e3a 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/DefaultCryptoService.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionService.kt @@ -16,56 +16,31 @@ package io.element.android.libraries.cryptography.impl -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.cryptography.api.CryptoService +import io.element.android.libraries.cryptography.api.AESEncryptionSpecs +import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.di.AppScope -import java.security.KeyStore import javax.crypto.Cipher -import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec import javax.inject.Inject -private const val ANDROID_KEYSTORE = "AndroidKeyStore" -private const val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM -private const val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE -private const val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES -private const val ENCRYPTION_AES_TRANSFORMATION = "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING" - +/** + * Default implementation of [EncryptionDecryptionService] using AES encryption. + */ @ContributesBinding(AppScope::class) -class DefaultCryptoService @Inject constructor() : CryptoService { - - override fun getOrCreateSecretKey(alias: String): SecretKey { - val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) - val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) - ?.secretKey - return if (secretKeyEntry == null) { - val generator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, ANDROID_KEYSTORE) - val keyGenSpec = KeyGenParameterSpec.Builder( - alias, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - ) - .setBlockModes(ENCRYPTION_BLOCK_MODE) - .setEncryptionPaddings(ENCRYPTION_PADDING) - .setKeySize(128) - .build() - generator.init(keyGenSpec) - generator.generateKey() - } else secretKeyEntry - } +class AESEncryptionDecryptionService @Inject constructor() : EncryptionDecryptionService { override fun createEncryptionCipher(key: SecretKey): Cipher { - return Cipher.getInstance(ENCRYPTION_AES_TRANSFORMATION).apply { + return Cipher.getInstance(AESEncryptionSpecs.CIPHER_TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, key) } } override fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher { val spec = GCMParameterSpec(128, initializationVector) - return Cipher.getInstance(ENCRYPTION_AES_TRANSFORMATION).apply { + return Cipher.getInstance(AESEncryptionSpecs.CIPHER_TRANSFORMATION).apply { init(Cipher.DECRYPT_MODE, key, spec) } } diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt new file mode 100644 index 0000000000..1da8bd7f1b --- /dev/null +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt @@ -0,0 +1,55 @@ +/* + * 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.cryptography.impl + +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import io.element.android.libraries.cryptography.api.AESEncryptionSpecs +import io.element.android.libraries.cryptography.api.SecretKeyProvider +import java.security.KeyStore +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +private const val ANDROID_KEYSTORE = "AndroidKeyStore" + +/** + * Default implementation of [SecretKeyProvider] that uses the Android Keystore to store the keys. + * The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode. + */ +class KeyStoreSecretKeyProvider : SecretKeyProvider { + + override fun getOrCreateKey(alias: String): SecretKey { + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) + ?.secretKey + return if (secretKeyEntry == null) { + val generator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM, ANDROID_KEYSTORE) + val keyGenSpec = KeyGenParameterSpec.Builder( + alias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(AESEncryptionSpecs.BLOCK_MODE) + .setEncryptionPaddings(AESEncryptionSpecs.PADDINGS) + .setKeySize(AESEncryptionSpecs.KEY_SIZE) + .build() + generator.init(keyGenSpec) + generator.generateKey() + } else { + secretKeyEntry + } + } +} diff --git a/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt b/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt new file mode 100644 index 0000000000..a693e29ef5 --- /dev/null +++ b/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt @@ -0,0 +1,54 @@ +/* + * 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.cryptography.impl + +import android.security.keystore.KeyProperties +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Test +import java.security.GeneralSecurityException +import javax.crypto.KeyGenerator + +class AESEncryptionDecryptionServiceTest { + + private val encryptionDecryptionService = AESEncryptionDecryptionService() + + @Test + fun given_a_valid_key_then_encrypt_decrypt_work() { + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) + keyGenerator.init(128) + val key = keyGenerator.generateKey() + val input = "Hello World".toByteArray() + val encryptionResult = encryptionDecryptionService.encrypt(key, input) + val decrypted = encryptionDecryptionService.decrypt(key, encryptionResult) + assertThat(decrypted).isEqualTo(input) + } + + @Test + fun given_a_wrong_key_then_decrypt_fail() { + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) + keyGenerator.init(128) + val encryptionKey = keyGenerator.generateKey() + val input = "Hello World".toByteArray() + val encryptionResult = encryptionDecryptionService.encrypt(encryptionKey, input) + val decryptionKey = keyGenerator.generateKey() + assertThrows(GeneralSecurityException::class.java) { + encryptionDecryptionService.decrypt(decryptionKey, encryptionResult) + } + } + +} diff --git a/libraries/cryptography/test/build.gradle.kts b/libraries/cryptography/test/build.gradle.kts new file mode 100644 index 0000000000..3b9074c897 --- /dev/null +++ b/libraries/cryptography/test/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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.cryptography.test" + + dependencies { + api(projects.libraries.cryptography.api) + } +} diff --git a/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt b/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt new file mode 100644 index 0000000000..d06a545d78 --- /dev/null +++ b/libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/SimpleSecretKeyProvider.kt @@ -0,0 +1,39 @@ +/* + * 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.cryptography.test + +import io.element.android.libraries.cryptography.api.AESEncryptionSpecs +import io.element.android.libraries.cryptography.api.SecretKeyProvider +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +class SimpleSecretKeyProvider : SecretKeyProvider { + + private var secretKeyForAlias = HashMap() + + override fun getOrCreateKey(alias: String): SecretKey { + return secretKeyForAlias.getOrPut(alias) { + generateKey() + } + } + + private fun generateKey(): SecretKey { + val keyGenerator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM) + keyGenerator.init(AESEncryptionSpecs.KEY_SIZE) + return keyGenerator.generateKey() + } +} From 1f97e95a3dc63c1494c33832203bcd272e89f5b4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 11:33:34 +0200 Subject: [PATCH 052/281] Fix warning --- .../features/lockscreen/impl/pin/PinCodeManager.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 960b6ecba1..5f84f5296d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -27,28 +27,30 @@ interface PinCodeManager { suspend fun isPinCodeAvailable(): Boolean /** - * creates a new encrypted pin code. + * Creates a new encrypted pin code. * @param pinCode the clear pin code to create */ suspend fun createPinCode(pinCode: String) /** - * @return true if the pin code is correct + * @return true if the pin code is correct. */ suspend fun verifyPinCode(pinCode: String): Boolean /** - * deletes the previously created pin code + * Deletes the previously created pin code. */ suspend fun deletePinCode() /** - * @return the number of remaining attempts before the pin code is blocked + * @return the number of remaining attempts before the pin code is blocked. */ suspend fun getRemainingPinCodeAttemptsNumber(): Int /** - * @return the number of remaining attempts before the pin code is blocked + * Should be called when the pin code is incorrect. + * Will decrement the remaining attempts number. + * @return the number of remaining attempts before the pin code is blocked. */ suspend fun onWrongPin(): Int From e5a8fd9635e067cb89df5bd6e259cca58ec04d90 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 18 Oct 2023 11:41:47 +0200 Subject: [PATCH 053/281] Make sure Konsist tests always run (#1590) * Make sure Konsist tests always run * Update tests/konsist/build.gradle.kts Co-authored-by: Benoit Marty --------- Co-authored-by: Benoit Marty --- tests/konsist/build.gradle.kts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/konsist/build.gradle.kts b/tests/konsist/build.gradle.kts index ca009bd89f..6658c0c5e0 100644 --- a/tests/konsist/build.gradle.kts +++ b/tests/konsist/build.gradle.kts @@ -32,3 +32,10 @@ dependencies { testImplementation(projects.libraries.architecture) testImplementation(projects.libraries.designsystem) } + +// Make sure Konsist tests are always run. This is needed because otherwise we'd have to either: +// - Add every single module as a dependency of this one. +// - Move the Konsist tests to the `app` module, but the `app` module does not need to know about Konsist. +tasks.withType().configureEach { + outputs.upToDateWhen { false } +} From 1d94b30b9fb98a50c3e08dff0f9ef600ba546ea2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 11:57:57 +0200 Subject: [PATCH 054/281] Revert settings.gradle.kts --- settings.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 590089adc6..105befcd04 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,5 @@ import java.net.URI -include(":libraries:cryptography:api") - /* * Copyright (c) 2022 New Vector Ltd * From c635281954d30ed7032beb713f32115d606d16ff Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 18 Oct 2023 13:22:53 +0000 Subject: [PATCH 055/281] Update screenshots --- ...h_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png} | 0 ...h_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png} | 0 ...pl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png} | 0 ...pl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.pin.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.pin.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png} (100%) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.pin.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-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.pin.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png From a896b42fa26e8449ac30205e4e4f215e972b06da Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 15:55:52 +0200 Subject: [PATCH 056/281] Pin : some clean up and fixes --- .../storage/SharedPreferencesPinCodeStore.kt | 23 +++++++++++++------ .../impl/pin/DefaultPinCodeManagerTest.kt | 6 ++--- .../impl/pin/storage/InMemoryPinCodeStore.kt | 6 +++-- .../impl/KeyStoreSecretKeyProvider.kt | 9 +++++++- .../AESEncryptionDecryptionServiceTest.kt | 4 ++-- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt index dbeabb53ed..27f4636400 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt @@ -22,6 +22,8 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -38,6 +40,7 @@ class SharedPreferencesPinCodeStore @Inject constructor( ) : PinCodeStore { private val listeners = CopyOnWriteArrayList() + private val mutex = Mutex() override suspend fun getEncryptedCode(): String? = withContext(dispatchers.io) { sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null) @@ -68,20 +71,26 @@ class SharedPreferencesPinCodeStore @Inject constructor( } override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) { - sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) + mutex.withLock { + sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) + } } override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { - val remaining = getRemainingPinCodeAttemptsNumber() - 1 - sharedPreferences.edit { - putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining) + mutex.withLock { + val remaining = getRemainingPinCodeAttemptsNumber() - 1 + sharedPreferences.edit { + putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining) + } + remaining } - remaining } override suspend fun resetCounter() = withContext(dispatchers.io) { - sharedPreferences.edit { - remove(REMAINING_PIN_CODE_ATTEMPTS_KEY) + mutex.withLock { + sharedPreferences.edit { + remove(REMAINING_PIN_CODE_ATTEMPTS_KEY) + } } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt index a3f29c5351..8b14d15e5e 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerTest.kt @@ -31,7 +31,7 @@ class DefaultPinCodeManagerTest { private val pinCodeManager = DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) @Test - fun given_a_pin_code_when_create_and_delete_assert_no_pin_code_left() = runTest { + fun `given a pin code when create and delete assert no pin code left`() = runTest { pinCodeManager.createPinCode("1234") assertThat(pinCodeManager.isPinCodeAvailable()).isTrue() pinCodeManager.deletePinCode() @@ -39,14 +39,14 @@ class DefaultPinCodeManagerTest { } @Test - fun given_a_pin_code_when_create_and_verify_with_the_same_pin_succeed() = runTest { + fun `given a pin code when create and verify with the same pin succeed`() = runTest { val pinCode = "1234" pinCodeManager.createPinCode(pinCode) assertThat(pinCodeManager.verifyPinCode(pinCode)).isTrue() } @Test - fun given_a_pin_code_when_create_and_verify_with_a_different_pin_fails() = runTest { + fun `given a pin code when create and verify with a different pin fails`() = runTest { pinCodeManager.createPinCode("1234") assertThat(pinCodeManager.verifyPinCode("1235")).isFalse() } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt index ed949c08de..0b7c2f256b 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt @@ -16,10 +16,12 @@ package io.element.android.features.lockscreen.impl.pin.storage +private const val DEFAULT_REMAINING_ATTEMPTS = 3 + class InMemoryPinCodeStore : PinCodeStore { private var pinCode: String? = null - private var remainingAttempts: Int = 3 + private var remainingAttempts: Int = DEFAULT_REMAINING_ATTEMPTS override suspend fun getRemainingPinCodeAttemptsNumber(): Int { return remainingAttempts @@ -30,7 +32,7 @@ class InMemoryPinCodeStore : PinCodeStore { } override suspend fun resetCounter() { - remainingAttempts = 3 + remainingAttempts = DEFAULT_REMAINING_ATTEMPTS } override fun addListener(listener: PinCodeStore.Listener) { diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt index 1da8bd7f1b..2cd09ea8f6 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt @@ -16,13 +16,17 @@ package io.element.android.libraries.cryptography.impl +import android.annotation.SuppressLint import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.cryptography.api.AESEncryptionSpecs import io.element.android.libraries.cryptography.api.SecretKeyProvider +import io.element.android.libraries.di.AppScope import java.security.KeyStore import javax.crypto.KeyGenerator import javax.crypto.SecretKey +import javax.inject.Inject private const val ANDROID_KEYSTORE = "AndroidKeyStore" @@ -30,8 +34,11 @@ private const val ANDROID_KEYSTORE = "AndroidKeyStore" * Default implementation of [SecretKeyProvider] that uses the Android Keystore to store the keys. * The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode. */ -class KeyStoreSecretKeyProvider : SecretKeyProvider { +@ContributesBinding(AppScope::class) +class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider { + // False positive lint issue + @SuppressLint("WrongConstant") override fun getOrCreateKey(alias: String): SecretKey { val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) diff --git a/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt b/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt index a693e29ef5..38e1c924ca 100644 --- a/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt +++ b/libraries/cryptography/impl/src/test/kotlin/io/element/android/libraries/cryptography/impl/AESEncryptionDecryptionServiceTest.kt @@ -28,7 +28,7 @@ class AESEncryptionDecryptionServiceTest { private val encryptionDecryptionService = AESEncryptionDecryptionService() @Test - fun given_a_valid_key_then_encrypt_decrypt_work() { + fun `given a valid key then encrypt decrypt work`() { val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) keyGenerator.init(128) val key = keyGenerator.generateKey() @@ -39,7 +39,7 @@ class AESEncryptionDecryptionServiceTest { } @Test - fun given_a_wrong_key_then_decrypt_fail() { + fun `given a wrong key then decrypt fail`() { val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) keyGenerator.init(128) val encryptionKey = keyGenerator.generateKey() From 436c9e83f00c48e5d9f3b46eec7d7c826f1800b3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 16:52:45 +0200 Subject: [PATCH 057/281] Fix kover... --- build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 704bbe6bbc..487776d948 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -251,9 +251,9 @@ koverMerged { // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" // Temporary until we have actually something to test. - excludes += "io.element.android.features.pin.impl.auth.PinAuthenticationPresenter" - excludes += "io.element.android.features.pin.impl.auth.PinAuthenticationPresenter$*" - excludes += "io.element.android.features.pin.impl.create.CreatePinPresenter" + excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter" + excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*" + excludes += "io.element.android.features.lockscreen.impl.create.CreatePinPresenter" } bound { minValue = 85 From a2e7baddc264de074c356ae878ce365e6260ebf1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 17:04:23 +0200 Subject: [PATCH 058/281] Pin : start create pin view --- .../lockscreen/impl/create/CreatePinView.kt | 71 +++++++++++++++---- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 120c0b6079..d86c2f296d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -14,31 +14,77 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.lockscreen.impl.create -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter +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.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.components.Text -import timber.log.Timber +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar @Composable fun CreatePinView( state: CreatePinState, + onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { - Timber.d("CreatePinView: $state") - Box(modifier, contentAlignment = Alignment.Center) { - Text( - "CreatePin feature view", - color = MaterialTheme.colorScheme.primary, - ) - } + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + BackButton(onClick = onBackClicked) + }, + title = {} + ) + }, + content = { padding -> + HeaderFooterPage( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding), + header = { CreatePinHeader() }, + footer = { CreatePinFooter() }, + ) + } + ) +} + +@Composable +private fun CreatePinHeader( + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier, + title = "Choose 4 digit PIN", + subTitle = "Lock Element to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app", + iconImageVector = Icons.Default.Lock, + ) +} + +@Composable +private fun CreatePinFooter() { + Button( + modifier = Modifier.fillMaxWidth(), + text = "Continue", + onClick = { + + } + ) } @Composable @@ -47,6 +93,7 @@ internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::clas ElementPreview { CreatePinView( state = state, + onBackClicked = {}, ) } } From 4416c0133a8937a368fb5f4defd05194f4383664 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Oct 2023 21:20:47 +0200 Subject: [PATCH 059/281] Create pin : start handling the text field --- .../lockscreen/impl/create/CreatePinEvents.kt | 2 +- .../lockscreen/impl/create/CreatePinNode.kt | 1 + .../impl/create/CreatePinPresenter.kt | 12 ++- .../lockscreen/impl/create/CreatePinState.kt | 3 + .../impl/create/CreatePinStateProvider.kt | 11 ++ .../lockscreen/impl/create/CreatePinView.kt | 102 ++++++++++++++++++ .../lockscreen/impl/create/model/PinDigit.kt | 29 +++++ .../lockscreen/impl/create/model/PinEntry.kt | 62 +++++++++++ .../designsystem/theme/ColorAliases.kt | 10 ++ 9 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index deb3095e69..f492f5ab05 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -17,5 +17,5 @@ package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { - object MyEvent : CreatePinEvents + data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt index 3689c0cc76..331d6ada84 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt @@ -38,6 +38,7 @@ class CreatePinNode @AssistedInject constructor( val state = presenter.present() CreatePinView( state = state, + onBackClicked = { }, modifier = modifier ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 08ba24e074..4599a06fc9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -17,6 +17,10 @@ package io.element.android.features.lockscreen.impl.create import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -24,14 +28,20 @@ class CreatePinPresenter @Inject constructor() : Presenter { @Composable override fun present(): CreatePinState { + val pinEntry by remember { + mutableStateOf(PinEntry.empty(4)) + } fun handleEvents(event: CreatePinEvents) { when (event) { - CreatePinEvents.MyEvent -> Unit + is CreatePinEvents.OnPinEntryChanged -> { + pinEntry.fillWith(event.entryAsText) + } } } return CreatePinState( + pinEntry = pinEntry, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 67311639ad..9b3835193f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -16,6 +16,9 @@ package io.element.android.features.lockscreen.impl.create +import io.element.android.features.lockscreen.impl.create.model.PinEntry + data class CreatePinState( + val pinEntry: PinEntry, val eventSink: (CreatePinEvents) -> Unit ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index a918b5193e..dbce5dddc5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -17,6 +17,9 @@ package io.element.android.features.lockscreen.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.lockscreen.impl.create.model.PinDigit +import io.element.android.features.lockscreen.impl.create.model.PinEntry +import kotlinx.collections.immutable.persistentListOf open class CreatePinStateProvider : PreviewParameterProvider { override val values: Sequence @@ -27,5 +30,13 @@ open class CreatePinStateProvider : PreviewParameterProvider { } fun aCreatePinState() = CreatePinState( + pinEntry = PinEntry( + digits = persistentListOf( + PinDigit.Filled('1'), + PinDigit.Filled('2'), + PinDigit.Empty, + PinDigit.Empty, + ) + ), eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index d86c2f296d..6035f9d5d6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -18,15 +18,30 @@ package io.element.android.features.lockscreen.impl.create +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.create.model.PinDigit +import io.element.android.features.lockscreen.impl.create.model.PinEntry 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.button.BackButton @@ -34,7 +49,10 @@ 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.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.theme.pinDigitBg +import io.element.android.libraries.theme.ElementTheme @Composable fun CreatePinView( @@ -59,6 +77,7 @@ fun CreatePinView( .consumeWindowInsets(padding), header = { CreatePinHeader() }, footer = { CreatePinFooter() }, + content = { CreatePinContent(state) } ) } ) @@ -87,6 +106,89 @@ private fun CreatePinFooter() { ) } +@Composable +private fun CreatePinContent( + state: CreatePinState, + modifier: Modifier = Modifier, +) { + + PinEntryTextField( + state.pinEntry, + onValueChange = { + state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) + }, + modifier = modifier + .padding(top = 36.dp) + .fillMaxWidth() + ) +} + +@Composable +fun PinEntryTextField( + pinEntry: PinEntry, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + BasicTextField( + modifier = modifier, + value = TextFieldValue(pinEntry.toText()), + onValueChange = { + onValueChange(it.text) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + decorationBox = { + PinEntryRow(pinEntry = pinEntry) + } + ) +} + +@Composable +private fun PinEntryRow( + pinEntry: PinEntry, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + for (digit in pinEntry.digits) { + PinDigitView(digit = digit) + } + } +} + +@Composable +private fun PinDigitView( + digit: PinDigit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(8.dp) + val appearanceModifier = when (digit) { + PinDigit.Empty -> { + Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) + } + is PinDigit.Filled -> { + Modifier.background(ElementTheme.colors.pinDigitBg, shape) + } + } + Box( + modifier = modifier + .size(40.dp, 50.dp) + .then(appearanceModifier), + contentAlignment = Alignment.Center, + + ) { + if (digit is PinDigit.Filled) { + Text( + text = digit.toText(), + style = ElementTheme.typography.fontHeadingMdBold + ) + } + + } +} + @Composable @PreviewsDayNight internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt new file mode 100644 index 0000000000..741a61cafe --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt @@ -0,0 +1,29 @@ +/* + * 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.lockscreen.impl.create.model + +sealed interface PinDigit { + data object Empty : PinDigit + data class Filled(val value: Char) : PinDigit + + fun toText(): String { + return when (this) { + is Empty -> "" + is Filled -> value.toString() + } + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt new file mode 100644 index 0000000000..587fde955d --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt @@ -0,0 +1,62 @@ +/* + * 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.lockscreen.impl.create.model + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +data class PinEntry( + val digits: ImmutableList, +) { + + companion object { + fun empty(size: Int): PinEntry { + val digits = List(size) { PinDigit.Empty } + return PinEntry( + digits = digits.toPersistentList() + ) + } + } + + private val size = digits.size + + /** + * Fill the first digits with the given text. + * Can't be more than the size of the PinEntry + * Keep the Empty digits at the end + * @return the new PinEntry + */ + fun fillWith(text: String): PinEntry { + val newDigits = digits.toMutableList() + text.forEachIndexed { index, char -> + if (index < size) { + newDigits[index] = PinDigit.Filled(char) + } + } + return copy(digits = newDigits.toPersistentList()) + } + + fun isPinComplete(): Boolean { + return digits.all { it is PinDigit.Filled } + } + + fun toText(): String { + return digits.joinToString("") { + it.toText() + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt index b347402b41..e85abb396b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -98,6 +98,16 @@ val SemanticColors.bgSubtleTertiary val SemanticColors.temporaryColorBgSpecial get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048) +// This color is not present in Semantic color, so put hard-coded value for now +val SemanticColors.pinDigitBg + get() = if (isLight) { + // We want LightDesignTokens.colorGray300 + Color(0xFFF0F2F5) + } else { + // We want DarkDesignTokens.colorGray400 + Color(0xFF26282D) + } + @PreviewsDayNight @Composable internal fun ColorAliasesPreview() = ElementPreview { From 1fd2fe1366364ab56d4b506067170691256eb346 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 20:35:06 +0000 Subject: [PATCH 060/281] Update dependency androidx.compose:compose-bom to v2023.10.01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a0bfc3a04..675148b7ff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ media3 = "1.1.1" browser = "1.6.0" # Compose -compose_bom = "2023.10.00" +compose_bom = "2023.10.01" composecompiler = "1.5.3" # Coroutines From 8d6ef153d90b4f938f1aa2769ebc33291067109c Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 18 Oct 2023 21:44:37 +0100 Subject: [PATCH 061/281] Fix switch and radio buttons toggling to invalid intermediate states. --- .../RoomNotificationSettingsEvents.kt | 3 +- .../RoomNotificationSettingsPresenter.kt | 108 ++++++++++++++---- .../RoomNotificationSettingsState.kt | 16 ++- .../RoomNotificationSettingsStateProvider.kt | 12 +- .../RoomNotificationSettingsView.kt | 32 ++++-- ...UserDefinedRoomNotificationSettingsView.kt | 13 ++- 6 files changed, 134 insertions(+), 50 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt index c69896a98b..8b3c25d267 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsEvents.kt @@ -22,5 +22,6 @@ sealed interface RoomNotificationSettingsEvents { data class RoomNotificationModeChanged(val mode: RoomNotificationMode) : RoomNotificationSettingsEvents data class SetNotificationMode(val isDefault: Boolean): RoomNotificationSettingsEvents data object DeleteCustomNotification: RoomNotificationSettingsEvents - data object ClearError: RoomNotificationSettingsEvents + data object ClearSetNotificationError: RoomNotificationSettingsEvents + data object ClearRestoreDefaultError: RoomNotificationSettingsEvents } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt index 9fb0c8b1c8..3086d878c4 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsPresenter.kt @@ -19,8 +19,6 @@ package io.element.android.features.roomdetails.impl.notificationsettings 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 @@ -31,7 +29,7 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.api.room.roomNotificationSettings +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce @@ -51,76 +49,136 @@ class RoomNotificationSettingsPresenter @Inject constructor( mutableStateOf(null) } val localCoroutineScope = rememberCoroutineScope() - val changeNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } - val deleteCustomNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val setNotificationSettingAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val restoreDefaultAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + + val roomNotificationSettings: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + + // We store state of which mode the user has set via the notification service before the new push settings have been updated. + // We show this state immediately to the user and debounce updates to notification settings to hide some invalid states returned + // by the rust sdk during these two events that cause the radio buttons ot toggle quickly back and forth. + // This is a client side work-around until bulk push rule updates are supported. + // ref: https://github.com/matrix-org/matrix-spec-proposals/pull/3934 + val pendingRoomNotificationMode: MutableState = remember { + mutableStateOf(null) + } + + // We store state of whether the user has set the notifications settings to default or custom via the notification service. + // We show this state immediately to the user and debounce updates to notification settings to hide some invalid states returned + // by the rust sdk during these two events that cause the switch ot toggle quickly back and forth. + // This is a client side work-around until bulk push rule updates are supported. + // ref: https://github.com/matrix-org/matrix-spec-proposals/pull/3934 + val pendingSetDefault: MutableState = remember { + mutableStateOf(null) + } LaunchedEffect(Unit) { getDefaultRoomNotificationMode(defaultRoomNotificationMode) - room.updateRoomNotificationSettings() - observeNotificationSettings() + fetchNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings) + observeNotificationSettings(pendingRoomNotificationMode, roomNotificationSettings) } - val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState() - fun handleEvents(event: RoomNotificationSettingsEvents) { when (event) { is RoomNotificationSettingsEvents.RoomNotificationModeChanged -> { - localCoroutineScope.setRoomNotificationMode(event.mode, changeNotificationSettingAction) + localCoroutineScope.setRoomNotificationMode(event.mode, pendingRoomNotificationMode, pendingSetDefault, setNotificationSettingAction) } is RoomNotificationSettingsEvents.SetNotificationMode -> { if (event.isDefault) { - localCoroutineScope.restoreDefaultRoomNotificationMode(changeNotificationSettingAction) + localCoroutineScope.restoreDefaultRoomNotificationMode(restoreDefaultAction, pendingSetDefault) } else { defaultRoomNotificationMode.value?.let { - localCoroutineScope.setRoomNotificationMode(it, changeNotificationSettingAction) + localCoroutineScope.setRoomNotificationMode(it, pendingRoomNotificationMode, pendingSetDefault, setNotificationSettingAction) } } } is RoomNotificationSettingsEvents.DeleteCustomNotification -> { - localCoroutineScope.restoreDefaultRoomNotificationMode(deleteCustomNotificationSettingAction) + localCoroutineScope.restoreDefaultRoomNotificationMode(restoreDefaultAction, pendingSetDefault) } - RoomNotificationSettingsEvents.ClearError -> { - changeNotificationSettingAction.value = Async.Uninitialized + RoomNotificationSettingsEvents.ClearSetNotificationError -> { + setNotificationSettingAction.value = Async.Uninitialized + } + RoomNotificationSettingsEvents.ClearRestoreDefaultError -> { + restoreDefaultAction.value = Async.Uninitialized } } } return RoomNotificationSettingsState( roomName = room.displayName, - roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), + roomNotificationSettings = roomNotificationSettings.value, + pendingRoomNotificationMode = pendingRoomNotificationMode.value, + pendingSetDefault = pendingSetDefault.value, defaultRoomNotificationMode = defaultRoomNotificationMode.value, - changeNotificationSettingAction = changeNotificationSettingAction.value, - deleteCustomNotificationSettingAction = deleteCustomNotificationSettingAction.value, + setNotificationSettingAction = setNotificationSettingAction.value, + restoreDefaultAction = restoreDefaultAction.value, eventSink = ::handleEvents, ) } @OptIn(FlowPreview::class) - private fun CoroutineScope.observeNotificationSettings() { + private fun CoroutineScope.observeNotificationSettings( + pendingModeState: MutableState, + roomNotificationSettings: MutableState> + ) { notificationSettingsService.notificationSettingsChangeFlow .debounce(0.5.seconds) .onEach { - room.updateRoomNotificationSettings() + fetchNotificationSettings(pendingModeState, roomNotificationSettings) } .launchIn(this) } - private fun CoroutineScope.getDefaultRoomNotificationMode(defaultRoomNotificationMode: MutableState) = launch { + private fun CoroutineScope.fetchNotificationSettings( + pendingModeState: MutableState, + roomNotificationSettings: MutableState> + ) = launch { + suspend { + pendingModeState.value = null + notificationSettingsService.getRoomNotificationSettings(room.roomId, room.isEncrypted, room.isOneToOne).getOrThrow() + }.runCatchingUpdatingState(roomNotificationSettings) + } + + private fun CoroutineScope.getDefaultRoomNotificationMode( + defaultRoomNotificationMode: MutableState + ) = launch { defaultRoomNotificationMode.value = notificationSettingsService.getDefaultRoomNotificationMode( room.isEncrypted, room.isOneToOne ).getOrThrow() } - private fun CoroutineScope.setRoomNotificationMode(mode: RoomNotificationMode, action: MutableState>) = launch { + private fun CoroutineScope.setRoomNotificationMode( + mode: RoomNotificationMode, + pendingModeState: MutableState, + pendingDefaultState: MutableState, + action: MutableState> + ) = launch { suspend { - notificationSettingsService.setRoomNotificationMode(room.roomId, mode).getOrThrow() + pendingModeState.value = mode + pendingDefaultState.value = false + val result = notificationSettingsService.setRoomNotificationMode(room.roomId, mode) + if (result.isFailure) { + pendingModeState.value = null + pendingDefaultState.value = null + } + result.getOrThrow() }.runCatchingUpdatingState(action) } - private fun CoroutineScope.restoreDefaultRoomNotificationMode(action: MutableState>) = launch { + private fun CoroutineScope.restoreDefaultRoomNotificationMode( + action: MutableState>, + pendingDefaultState: MutableState + ) = launch { suspend { - notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId).getOrThrow() + pendingDefaultState.value = true + val result = notificationSettingsService.restoreDefaultRoomNotificationMode(room.roomId) + if (result.isFailure) { + pendingDefaultState.value = null + } + result.getOrThrow() }.runCatchingUpdatingState(action) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt index a7c5c3b883..1f8c7e4ce8 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsState.kt @@ -22,9 +22,19 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings data class RoomNotificationSettingsState( val roomName: String, - val roomNotificationSettings: RoomNotificationSettings?, + val roomNotificationSettings: Async, + val pendingRoomNotificationMode: RoomNotificationMode?, + val pendingSetDefault: Boolean?, val defaultRoomNotificationMode: RoomNotificationMode?, - val changeNotificationSettingAction: Async, - val deleteCustomNotificationSettingAction: Async, + val setNotificationSettingAction: Async, + val restoreDefaultAction: Async, val eventSink: (RoomNotificationSettingsEvents) -> Unit ) + +val RoomNotificationSettingsState.displayNotificationMode: RoomNotificationMode? get() { + return pendingRoomNotificationMode ?: roomNotificationSettings.dataOrNull()?.mode +} + +val RoomNotificationSettingsState.displayIsDefault: Boolean? get() { + return pendingSetDefault ?: roomNotificationSettings.dataOrNull()?.isDefault +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt index 220d82f6b5..961909f933 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt @@ -26,12 +26,14 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider< get() = sequenceOf( RoomNotificationSettingsState( roomName = "Room 1", - RoomNotificationSettings( + Async.Success(RoomNotificationSettings( mode = RoomNotificationMode.MUTE, - isDefault = true), - RoomNotificationMode.ALL_MESSAGES, - changeNotificationSettingAction = Async.Uninitialized, - deleteCustomNotificationSettingAction = Async.Uninitialized, + isDefault = true)), + pendingRoomNotificationMode = null, + pendingSetDefault = null, + defaultRoomNotificationMode = RoomNotificationMode.ALL_MESSAGES, + setNotificationSettingAction = Async.Uninitialized, + restoreDefaultAction = Async.Uninitialized, eventSink = { }, ), ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt index 92bfe0c084..6f440958b2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -75,27 +75,29 @@ fun RoomNotificationSettingsView( null -> "" } + val roomNotificationSettings = state.roomNotificationSettings.dataOrNull() + PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) { PreferenceSwitch( - isChecked = state.roomNotificationSettings?.isDefault.orTrue(), + isChecked = state.displayIsDefault.orTrue(), onCheckedChange = { state.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(it)) }, title = "Match default setting", subtitle = subtitle, - enabled = state.roomNotificationSettings != null + enabled = roomNotificationSettings != null ) PreferenceText( title = stringResource(id = R.string.screen_room_notification_settings_allow_custom), subtitle = stringResource(id = R.string.screen_room_notification_settings_allow_custom_footnote), - enabled = state.roomNotificationSettings != null && !state.roomNotificationSettings.isDefault, + enabled = !state.displayIsDefault.orTrue(), ) - if (state.roomNotificationSettings != null) { + if (roomNotificationSettings != null && state.displayNotificationMode != null) { RoomNotificationSettingsOptions( - selected = state.roomNotificationSettings.mode, - enabled = !state.roomNotificationSettings.isDefault, + selected = state.displayNotificationMode, + enabled = !state.displayIsDefault.orTrue(), onOptionSelected = { state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) }, @@ -103,12 +105,22 @@ fun RoomNotificationSettingsView( } } - when (state.changeNotificationSettingAction) { + when (state.setNotificationSettingAction) { is Async.Loading -> { ProgressDialog() } is Async.Failure -> { - ShowChangeNotificationSettingError(state) + ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearSetNotificationError) + } + else -> Unit + } + + when (state.restoreDefaultAction) { + is Async.Loading -> { + ProgressDialog() + } + is Async.Failure -> { + ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearRestoreDefaultError) } else -> Unit } @@ -155,11 +167,11 @@ fun RoomNotificationSettingsOptions( } @Composable -fun ShowChangeNotificationSettingError(state: RoomNotificationSettingsState) { +fun ShowChangeNotificationSettingError(state: RoomNotificationSettingsState, event: RoomNotificationSettingsEvents) { ErrorDialog( title = stringResource(CommonStrings.dialog_title_error), content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode), - onDismiss = { state.eventSink(RoomNotificationSettingsEvents.ClearError) }, + onDismiss = { state.eventSink(event) }, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt index 6435e64497..aa8a2a739d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt @@ -62,10 +62,11 @@ fun UserDefinedRoomNotificationSettingsView( .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - if (state.roomNotificationSettings != null) { + val roomNotificationSettings = state.roomNotificationSettings.dataOrNull() + if (roomNotificationSettings != null && state.displayNotificationMode != null) { RoomNotificationSettingsOptions( - selected = state.roomNotificationSettings.mode, - enabled = !state.roomNotificationSettings.isDefault, + selected = state.displayNotificationMode, + enabled = roomNotificationSettings.isDefault, onOptionSelected = { state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) }, @@ -81,7 +82,7 @@ fun UserDefinedRoomNotificationSettingsView( } ) - when (state.changeNotificationSettingAction) { + when (state.setNotificationSettingAction) { is Async.Loading -> { ProgressDialog() } @@ -91,7 +92,7 @@ fun UserDefinedRoomNotificationSettingsView( else -> Unit } - when (state.deleteCustomNotificationSettingAction) { + when (state.restoreDefaultAction) { is Async.Loading -> { ProgressDialog() } @@ -99,7 +100,7 @@ fun UserDefinedRoomNotificationSettingsView( ShowChangeNotificationSettingError(state) } is Async.Success -> { - LaunchedEffect(state.deleteCustomNotificationSettingAction) { + LaunchedEffect(state.restoreDefaultAction) { onBackPressed() } } From b5ca65ed0f5d60a839d70a9ba45c08287b48f040 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 18 Oct 2023 22:30:29 +0100 Subject: [PATCH 062/281] Fix enabled state and ClearError events. --- .../UserDefinedRoomNotificationSettingsView.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt index aa8a2a739d..75642f4e15 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt @@ -32,13 +32,14 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.Async -import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.preferences.PreferenceText 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.designsystem.utils.CommonDrawables @Composable fun UserDefinedRoomNotificationSettingsView( @@ -66,7 +67,7 @@ fun UserDefinedRoomNotificationSettingsView( if (roomNotificationSettings != null && state.displayNotificationMode != null) { RoomNotificationSettingsOptions( selected = state.displayNotificationMode, - enabled = roomNotificationSettings.isDefault, + enabled = !state.displayIsDefault.orTrue(), onOptionSelected = { state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) }, @@ -75,7 +76,7 @@ fun UserDefinedRoomNotificationSettingsView( PreferenceText( title = stringResource(R.string.screen_room_notification_settings_edit_remove_setting), - icon = ImageVector.vectorResource(VectorIcons.Delete), + icon = ImageVector.vectorResource(CommonDrawables.ic_compound_delete), tintColor = MaterialTheme.colorScheme.error, onClick = { state.eventSink(RoomNotificationSettingsEvents.DeleteCustomNotification) @@ -87,7 +88,7 @@ fun UserDefinedRoomNotificationSettingsView( ProgressDialog() } is Async.Failure -> { - ShowChangeNotificationSettingError(state) + ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearSetNotificationError) } else -> Unit } @@ -97,7 +98,7 @@ fun UserDefinedRoomNotificationSettingsView( ProgressDialog() } is Async.Failure -> { - ShowChangeNotificationSettingError(state) + ShowChangeNotificationSettingError(state, RoomNotificationSettingsEvents.ClearRestoreDefaultError) } is Async.Success -> { LaunchedEffect(state.restoreDefaultAction) { From 0ab13c613182e66f24ab6b1cda3ebb4b69ef9594 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:47:52 +0000 Subject: [PATCH 063/281] Update dependency androidx.sqlite:sqlite-ktx to v2.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a0bfc3a04..f231381a65 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -155,7 +155,7 @@ sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", ver sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4" -sqlite = "androidx.sqlite:sqlite-ktx:2.3.1" +sqlite = "androidx.sqlite:sqlite-ktx:2.4.0" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" From 9039186e2ce6ff4e3637f29e4d1db1be547e0775 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 01:04:02 +0000 Subject: [PATCH 064/281] Update dependency com.google.firebase:firebase-bom to v32.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a0bfc3a04..cd8e7b165c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.3.1" +google_firebase_bom = "com.google.firebase:firebase-bom:32.4.0" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } From a73378b0cec9aafd7b3abe0ea8d0bf765a805f07 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:51:21 +0200 Subject: [PATCH 065/281] Update dependency androidx.recyclerview:recyclerview to v1.3.2 (#1599) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a0bfc3a04..dd711b6d72 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ core = "1.12.0" datastore = "1.0.0" constraintlayout = "2.1.4" constraintlayout_compose = "1.0.1" -recyclerview = "1.3.1" +recyclerview = "1.3.2" lifecycle = "2.6.2" activity = "1.8.0" startup = "1.1.1" From 6bec6235b4ce7a2eddb4a0c866463a2b507ab36d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:48:23 +0200 Subject: [PATCH 066/281] Update dependency io.sentry:sentry-android to v6.32.0 (#1602) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd711b6d72..b2e91fce99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -167,7 +167,7 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" # Analytics posthog = "com.posthog.android:posthog:2.0.3" -sentry = "io.sentry:sentry-android:6.31.0" +sentry = "io.sentry:sentry-android:6.32.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f" # Emojibase From 9aa4c595165c1b1c9984aad3754d33a566861c3d Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 19 Oct 2023 10:49:11 +0200 Subject: [PATCH 067/281] Hide keyboard when exiting the room screen (#1593) --- changelog.d/1375.bugfix | 1 + .../android/features/messages/impl/MessagesView.kt | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog.d/1375.bugfix diff --git a/changelog.d/1375.bugfix b/changelog.d/1375.bugfix new file mode 100644 index 0000000000..d80ff3543c --- /dev/null +++ b/changelog.d/1375.bugfix @@ -0,0 +1 @@ +Hide keyboard when exiting the chat room screen. 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 971df06053..b79e84a2e0 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 @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -222,6 +223,14 @@ fun MessagesView( ReinviteDialog( state = state ) + + // Since the textfield is now based on an Android view, this is no longer done automatically. + // We need to hide the keyboard automatically when navigating out of this screen. + DisposableEffect(Unit) { + onDispose { + localView.hideKeyboard() + } + } } @Composable From 79d2941fe4b1617895d18dc4e935d724b621e1e4 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 19 Oct 2023 11:00:32 +0200 Subject: [PATCH 068/281] Include desugaring lib also in library modules (#1604) ## Type of change - [ ] Feature - [ ] Bugfix - [x] Technical - [ ] Other : ## Content Includes the `coreLibraryDesugaring(libs.android.desugar)` dependency in all modules which use one of our gradle plugins. ## Motivation and context Right now desugaring is enabled also in library modules but the desugar dependency is not included in those. This causes some unwanted side effects such as being unable to run compose previews in an emu. This change will also include the desugar dependency in those libraries. --- app/build.gradle.kts | 1 - libraries/pushstore/impl/build.gradle.kts | 2 -- libraries/session-storage/impl/build.gradle.kts | 2 -- plugins/src/main/kotlin/extension/CommonExtension.kt | 1 - .../kotlin/io.element.android-compose-application.gradle.kts | 4 ++++ .../main/kotlin/io.element.android-compose-library.gradle.kts | 4 ++++ plugins/src/main/kotlin/io.element.android-library.gradle.kts | 4 ++++ samples/minimal/build.gradle.kts | 1 - 8 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 839a5095dd..6ac84cfec2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -203,7 +203,6 @@ dependencies { implementation(projects.appnav) anvil(projects.anvilcodegen) - coreLibraryDesugaring(libs.android.desugar) implementation(libs.appyx.core) implementation(libs.androidx.splash) implementation(libs.androidx.core) diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts index 5946e77694..17e0268af1 100644 --- a/libraries/pushstore/impl/build.gradle.kts +++ b/libraries/pushstore/impl/build.gradle.kts @@ -55,6 +55,4 @@ dependencies { androidTestImplementation(libs.test.truth) androidTestImplementation(libs.test.runner) androidTestImplementation(projects.libraries.sessionStorage.test) - - coreLibraryDesugaring(libs.android.desugar) } diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 03de9acf86..cfbfa4c57d 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -45,8 +45,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.coroutines.test) testImplementation(libs.sqldelight.driver.jvm) - - coreLibraryDesugaring(libs.android.desugar) } sqldelight { diff --git a/plugins/src/main/kotlin/extension/CommonExtension.kt b/plugins/src/main/kotlin/extension/CommonExtension.kt index e3f7b3682e..97305dbc66 100644 --- a/plugins/src/main/kotlin/extension/CommonExtension.kt +++ b/plugins/src/main/kotlin/extension/CommonExtension.kt @@ -31,7 +31,6 @@ fun CommonExtension<*, *, *, *, *>.androidConfig(project: Project) { } compileOptions { - isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } diff --git a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts index af73409888..80bc0f884e 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts @@ -32,9 +32,13 @@ plugins { android { androidConfig(project) composeConfig(libs) + compileOptions { + isCoreLibraryDesugaringEnabled = true + } } dependencies { commonDependencies(libs) composeDependencies(libs) + coreLibraryDesugaring(libs.android.desugar) } diff --git a/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts index e420ab3c8d..3194505e4e 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts @@ -32,9 +32,13 @@ plugins { android { androidConfig(project) composeConfig(libs) + compileOptions { + isCoreLibraryDesugaringEnabled = true + } } dependencies { commonDependencies(libs) composeDependencies(libs) + coreLibraryDesugaring(libs.android.desugar) } diff --git a/plugins/src/main/kotlin/io.element.android-library.gradle.kts b/plugins/src/main/kotlin/io.element.android-library.gradle.kts index 6c3c77223c..f3a84031e6 100644 --- a/plugins/src/main/kotlin/io.element.android-library.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-library.gradle.kts @@ -29,8 +29,12 @@ plugins { android { androidConfig(project) + compileOptions { + isCoreLibraryDesugaringEnabled = true + } } dependencies { commonDependencies(libs) + coreLibraryDesugaring(libs.android.desugar) } diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 1473bd7f93..016989a2d6 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -65,5 +65,4 @@ dependencies { implementation(projects.services.toolbox.impl) implementation(projects.libraries.featureflag.impl) implementation(libs.coroutines.core) - coreLibraryDesugaring(libs.android.desugar) } From c15a193d4a9c236c8116d2f78156ccaa74fadfc9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 12:11:14 +0200 Subject: [PATCH 069/281] Pin create : add some more states to manage validation and confirmation --- .../lockscreen/impl/create/CreatePinEvents.kt | 1 + .../impl/create/CreatePinPresenter.kt | 55 +++++++++++++++++-- .../lockscreen/impl/create/CreatePinState.kt | 14 ++++- .../impl/create/CreatePinStateProvider.kt | 31 +++++++---- .../lockscreen/impl/create/CreatePinView.kt | 10 ++-- .../lockscreen/impl/create/model/PinEntry.kt | 4 ++ .../create/validation/PinCreationFailure.kt | 22 ++++++++ .../impl/create/validation/PinValidator.kt | 40 ++++++++++++++ 8 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index f492f5ab05..9e53762c07 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -18,4 +18,5 @@ package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents + data object OnClearValidationFailure : CreatePinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 4599a06fc9..18a17acb62 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -20,28 +20,73 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure +import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.libraries.architecture.Presenter import javax.inject.Inject -class CreatePinPresenter @Inject constructor() : Presenter { +private const val PIN_SIZE = 4 + +class CreatePinPresenter @Inject constructor( + private val pinValidator: PinValidator, + private val pinCodeManager: PinCodeManager, +) : Presenter { @Composable override fun present(): CreatePinState { - val pinEntry by remember { - mutableStateOf(PinEntry.empty(4)) + var choosePinEntry by remember { + mutableStateOf(PinEntry.empty(PIN_SIZE)) + } + var confirmPinEntry by remember { + mutableStateOf(PinEntry.empty(PIN_SIZE)) + } + var isConfirmationStep by remember { + mutableStateOf(false) + } + var creationFailure by remember { + mutableStateOf(null) } fun handleEvents(event: CreatePinEvents) { when (event) { is CreatePinEvents.OnPinEntryChanged -> { - pinEntry.fillWith(event.entryAsText) + if (isConfirmationStep) { + confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) + if (confirmPinEntry.isPinComplete()) { + if (confirmPinEntry == choosePinEntry) { + //pinCodeManager.savePin(confirmPinEntry.toText()) + } else { + confirmPinEntry = PinEntry.empty(PIN_SIZE) + creationFailure = PinCreationFailure.ConfirmationPinNotMatching + } + } + } else { + choosePinEntry = choosePinEntry.fillWith(event.entryAsText) + if (choosePinEntry.isPinComplete()) { + when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { + is PinValidator.Result.Invalid -> { + choosePinEntry = PinEntry.empty(PIN_SIZE) + creationFailure = pinValidationResult.failure + } + PinValidator.Result.Valid -> isConfirmationStep = true + } + } + } + } + CreatePinEvents.OnClearValidationFailure -> { + creationFailure = null } } } return CreatePinState( - pinEntry = pinEntry, + choosePinEntry = choosePinEntry, + confirmPinEntry = confirmPinEntry, + isConfirmationStep = isConfirmationStep, + creationFailure = creationFailure, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 9b3835193f..799d4b20a8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -17,8 +17,18 @@ package io.element.android.features.lockscreen.impl.create import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure data class CreatePinState( - val pinEntry: PinEntry, + val choosePinEntry: PinEntry, + val confirmPinEntry: PinEntry, + val isConfirmationStep: Boolean, + val creationFailure: PinCreationFailure?, val eventSink: (CreatePinEvents) -> Unit -) +) { + val activePinEntry = if (isConfirmationStep) { + confirmPinEntry + } else { + choosePinEntry + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index dbce5dddc5..f4d778a296 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -17,26 +17,33 @@ package io.element.android.features.lockscreen.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry -import kotlinx.collections.immutable.persistentListOf +import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure open class CreatePinStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aCreatePinState(), - // Add other states here + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("12") + ), + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("1789"), + isConfirmationStep = true, + ), ) } -fun aCreatePinState() = CreatePinState( - pinEntry = PinEntry( - digits = persistentListOf( - PinDigit.Filled('1'), - PinDigit.Filled('2'), - PinDigit.Empty, - PinDigit.Empty, - ) - ), +fun aCreatePinState( + choosePinEntry: PinEntry = PinEntry.empty(4), + confirmPinEntry: PinEntry = PinEntry.empty(4), + isConfirmationStep: Boolean = false, + creationFailure: PinCreationFailure? = null, +) = CreatePinState( + choosePinEntry = choosePinEntry, + confirmPinEntry = confirmPinEntry, + isConfirmationStep = isConfirmationStep, + creationFailure = creationFailure, eventSink = {} ) + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 6035f9d5d6..f5b2e49df8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -75,7 +75,7 @@ fun CreatePinView( modifier = Modifier .padding(padding) .consumeWindowInsets(padding), - header = { CreatePinHeader() }, + header = { CreatePinHeader(state.isConfirmationStep) }, footer = { CreatePinFooter() }, content = { CreatePinContent(state) } ) @@ -85,11 +85,12 @@ fun CreatePinView( @Composable private fun CreatePinHeader( + isValidationStep: Boolean, modifier: Modifier = Modifier, ) { IconTitleSubtitleMolecule( modifier = modifier, - title = "Choose 4 digit PIN", + title = if (isValidationStep) "Confirm PIN" else "Choose 4 digit PIN", subTitle = "Lock Element to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app", iconImageVector = Icons.Default.Lock, ) @@ -111,9 +112,8 @@ private fun CreatePinContent( state: CreatePinState, modifier: Modifier = Modifier, ) { - PinEntryTextField( - state.pinEntry, + state.activePinEntry, onValueChange = { state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) }, @@ -135,7 +135,7 @@ fun PinEntryTextField( onValueChange = { onValueChange(it.text) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), decorationBox = { PinEntryRow(pinEntry = pinEntry) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt index 587fde955d..2228110156 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt @@ -50,6 +50,10 @@ data class PinEntry( return copy(digits = newDigits.toPersistentList()) } + fun clear(): PinEntry { + return fillWith("") + } + fun isPinComplete(): Boolean { return digits.all { it is PinDigit.Filled } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt new file mode 100644 index 0000000000..26b1eb5fd8 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt @@ -0,0 +1,22 @@ +/* + * 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.lockscreen.impl.create.validation + +sealed interface PinCreationFailure { + data object ChosenPinBlacklisted : PinCreationFailure + data object ConfirmationPinNotMatching : PinCreationFailure +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt new file mode 100644 index 0000000000..1d97cda60d --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt @@ -0,0 +1,40 @@ +/* + * 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.lockscreen.impl.create.validation + +import io.element.android.features.lockscreen.impl.create.model.PinEntry +import javax.inject.Inject + +private val BLACKLIST = listOf("0000", "1234") + +class PinValidator @Inject constructor() { + + sealed interface Result { + data object Valid : Result + data class Invalid(val failure: PinCreationFailure) : Result + } + + fun isPinValid(pinEntry: PinEntry): Result { + val pinAsText = pinEntry.toText() + val isBlacklisted = BLACKLIST.any { it == pinAsText } + return if (isBlacklisted) { + Result.Invalid(PinCreationFailure.ChosenPinBlacklisted) + } else { + Result.Valid + } + } +} From 7b611e7c7ab75738a12cba75533c29da9894f00d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 12:16:30 +0200 Subject: [PATCH 070/281] Pin create : improve clear validation --- .../lockscreen/impl/create/CreatePinPresenter.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 18a17acb62..a39c199256 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -59,7 +59,6 @@ class CreatePinPresenter @Inject constructor( if (confirmPinEntry == choosePinEntry) { //pinCodeManager.savePin(confirmPinEntry.toText()) } else { - confirmPinEntry = PinEntry.empty(PIN_SIZE) creationFailure = PinCreationFailure.ConfirmationPinNotMatching } } @@ -68,7 +67,6 @@ class CreatePinPresenter @Inject constructor( if (choosePinEntry.isPinComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { - choosePinEntry = PinEntry.empty(PIN_SIZE) creationFailure = pinValidationResult.failure } PinValidator.Result.Valid -> isConfirmationStep = true @@ -77,6 +75,17 @@ class CreatePinPresenter @Inject constructor( } } CreatePinEvents.OnClearValidationFailure -> { + when (creationFailure) { + is PinCreationFailure.ConfirmationPinNotMatching -> { + choosePinEntry = PinEntry.empty(PIN_SIZE) + confirmPinEntry = PinEntry.empty(PIN_SIZE) + } + is PinCreationFailure.ChosenPinBlacklisted -> { + choosePinEntry = PinEntry.empty(PIN_SIZE) + } + null -> Unit + } + isConfirmationStep = false creationFailure = null } } From 539854b18ce738465ef8dc81f04fbd94c8423912 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 12:18:26 +0200 Subject: [PATCH 071/281] Create pin : remove PinCodeManager and add TODO --- .../features/lockscreen/impl/create/CreatePinPresenter.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index a39c199256..435de0ebe0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure import io.element.android.features.lockscreen.impl.create.validation.PinValidator -import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -32,7 +31,6 @@ private const val PIN_SIZE = 4 class CreatePinPresenter @Inject constructor( private val pinValidator: PinValidator, - private val pinCodeManager: PinCodeManager, ) : Presenter { @Composable @@ -57,7 +55,7 @@ class CreatePinPresenter @Inject constructor( confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) if (confirmPinEntry.isPinComplete()) { if (confirmPinEntry == choosePinEntry) { - //pinCodeManager.savePin(confirmPinEntry.toText()) + //TODO save in db and navigate to next screen } else { creationFailure = PinCreationFailure.ConfirmationPinNotMatching } From caf6d6b674a54a0b850e9b85f32f5ce58b214f2d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 12:34:29 +0200 Subject: [PATCH 072/281] Create pin : render failures --- .../lockscreen/impl/create/CreatePinEvents.kt | 2 +- .../impl/create/CreatePinPresenter.kt | 22 +++++----- .../lockscreen/impl/create/CreatePinState.kt | 4 +- .../impl/create/CreatePinStateProvider.kt | 17 ++++++-- .../lockscreen/impl/create/CreatePinView.kt | 41 +++++++++++++------ ...CreationFailure.kt => CreatePinFailure.kt} | 6 +-- .../impl/create/validation/PinValidator.kt | 4 +- 7 files changed, 61 insertions(+), 35 deletions(-) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/{PinCreationFailure.kt => CreatePinFailure.kt} (80%) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt index 9e53762c07..78ce529325 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt @@ -18,5 +18,5 @@ package io.element.android.features.lockscreen.impl.create sealed interface CreatePinEvents { data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents - data object OnClearValidationFailure : CreatePinEvents + data object ClearFailure : CreatePinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 435de0ebe0..525b80314b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.create.validation.PinValidator import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -44,8 +44,8 @@ class CreatePinPresenter @Inject constructor( var isConfirmationStep by remember { mutableStateOf(false) } - var creationFailure by remember { - mutableStateOf(null) + var createPinFailure by remember { + mutableStateOf(null) } fun handleEvents(event: CreatePinEvents) { @@ -57,7 +57,7 @@ class CreatePinPresenter @Inject constructor( if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { - creationFailure = PinCreationFailure.ConfirmationPinNotMatching + createPinFailure = CreatePinFailure.ConfirmationPinNotMatching } } } else { @@ -65,26 +65,26 @@ class CreatePinPresenter @Inject constructor( if (choosePinEntry.isPinComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { - creationFailure = pinValidationResult.failure + createPinFailure = pinValidationResult.failure } PinValidator.Result.Valid -> isConfirmationStep = true } } } } - CreatePinEvents.OnClearValidationFailure -> { - when (creationFailure) { - is PinCreationFailure.ConfirmationPinNotMatching -> { + CreatePinEvents.ClearFailure -> { + when (createPinFailure) { + is CreatePinFailure.ConfirmationPinNotMatching -> { choosePinEntry = PinEntry.empty(PIN_SIZE) confirmPinEntry = PinEntry.empty(PIN_SIZE) } - is PinCreationFailure.ChosenPinBlacklisted -> { + is CreatePinFailure.ChosenPinBlacklisted -> { choosePinEntry = PinEntry.empty(PIN_SIZE) } null -> Unit } isConfirmationStep = false - creationFailure = null + createPinFailure = null } } } @@ -93,7 +93,7 @@ class CreatePinPresenter @Inject constructor( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - creationFailure = creationFailure, + createPinFailure = createPinFailure, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 799d4b20a8..914e12ca96 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -17,13 +17,13 @@ package io.element.android.features.lockscreen.impl.create import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure data class CreatePinState( val choosePinEntry: PinEntry, val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, - val creationFailure: PinCreationFailure?, + val createPinFailure: CreatePinFailure?, val eventSink: (CreatePinEvents) -> Unit ) { val activePinEntry = if (isConfirmationStep) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index f4d778a296..40287622fd 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -18,7 +18,7 @@ package io.element.android.features.lockscreen.impl.create import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.PinCreationFailure +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure open class CreatePinStateProvider : PreviewParameterProvider { override val values: Sequence @@ -31,6 +31,17 @@ open class CreatePinStateProvider : PreviewParameterProvider { choosePinEntry = PinEntry.empty(4).fillWith("1789"), isConfirmationStep = true, ), + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("1789"), + confirmPinEntry = PinEntry.empty(4).fillWith("1788"), + isConfirmationStep = true, + creationFailure = CreatePinFailure.ConfirmationPinNotMatching + ), + aCreatePinState( + choosePinEntry = PinEntry.empty(4).fillWith("1111"), + creationFailure = CreatePinFailure.ChosenPinBlacklisted + ), + ) } @@ -38,12 +49,12 @@ fun aCreatePinState( choosePinEntry: PinEntry = PinEntry.empty(4), confirmPinEntry: PinEntry = PinEntry.empty(4), isConfirmationStep: Boolean = false, - creationFailure: PinCreationFailure? = null, + creationFailure: CreatePinFailure? = null, ) = CreatePinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - creationFailure = creationFailure, + createPinFailure = creationFailure, eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index f5b2e49df8..fdce08c229 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -42,12 +42,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure 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.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog 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.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -76,7 +77,6 @@ fun CreatePinView( .padding(padding) .consumeWindowInsets(padding), header = { CreatePinHeader(state.isConfirmationStep) }, - footer = { CreatePinFooter() }, content = { CreatePinContent(state) } ) } @@ -96,17 +96,6 @@ private fun CreatePinHeader( ) } -@Composable -private fun CreatePinFooter() { - Button( - modifier = Modifier.fillMaxWidth(), - text = "Continue", - onClick = { - - } - ) -} - @Composable private fun CreatePinContent( state: CreatePinState, @@ -121,6 +110,32 @@ private fun CreatePinContent( .padding(top = 36.dp) .fillMaxWidth() ) + if (state.createPinFailure != null) { + ErrorDialog( + modifier = modifier, + title = state.createPinFailure.title(), + content = state.createPinFailure.content(), + onDismiss = { + state.eventSink(CreatePinEvents.ClearFailure) + } + ) + } +} + +@Composable +private fun CreatePinFailure.content(): String { + return when (this) { + CreatePinFailure.ChosenPinBlacklisted -> "You cannot choose this as your PIN code for security reasons" + CreatePinFailure.ConfirmationPinNotMatching -> "Please enter the same PIN twice" + } +} + +@Composable +private fun CreatePinFailure.title(): String { + return when (this) { + CreatePinFailure.ChosenPinBlacklisted -> "Choose a different PIN" + CreatePinFailure.ConfirmationPinNotMatching -> "PINs don't match" + } } @Composable diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt index 26b1eb5fd8..96c0de0056 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinCreationFailure.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt @@ -16,7 +16,7 @@ package io.element.android.features.lockscreen.impl.create.validation -sealed interface PinCreationFailure { - data object ChosenPinBlacklisted : PinCreationFailure - data object ConfirmationPinNotMatching : PinCreationFailure +sealed interface CreatePinFailure { + data object ChosenPinBlacklisted : CreatePinFailure + data object ConfirmationPinNotMatching : CreatePinFailure } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt index 1d97cda60d..8c1854ecee 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt @@ -25,14 +25,14 @@ class PinValidator @Inject constructor() { sealed interface Result { data object Valid : Result - data class Invalid(val failure: PinCreationFailure) : Result + data class Invalid(val failure: CreatePinFailure) : Result } fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() val isBlacklisted = BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { - Result.Invalid(PinCreationFailure.ChosenPinBlacklisted) + Result.Invalid(CreatePinFailure.ChosenPinBlacklisted) } else { Result.Valid } From 3a4d32f79df62b4b09925cc000e4c5e26d8c2b43 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 19 Oct 2023 10:47:03 +0000 Subject: [PATCH 073/281] Update screenshots --- ...create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png | 3 +++ ...create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png | 3 +++ 10 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png index e8dd1a28d7..03b059496a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9914a33ba23544bdfce1e21b52ad247024392730fb22b60bc9b6fa6440f004d4 -size 9216 +oid sha256:df5f2cc45255cec07dc99de8df0f2e8dd06fdc3afded3ba43c8deec2bb7c1d0b +size 34529 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ffcb20390e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c5f338938adeeb280be0b68f57b5ba0b4c9a904e8660e99fb6e57cd6b4774fc +size 34534 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5e82145176 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a412e9c38549341f707e9a6379e165d554294c0f452fec82ab783fa9e0533ad7 +size 32374 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2fdd3802d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fbba51aa681041018dd41f9f2e69159e229411b2cf438a62367a7f1edbb857a +size 28781 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..26305ae91e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:843141e92c23f9cbba98296fc543c365d33a708daaabe0ff9549ccb8d6f69af4 +size 36969 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png index 157a7c52c3..c3a42144c6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ad524a918e499fcea6fd5293358167ff52f8877cc31b778c8def01925fa662f -size 8582 +oid sha256:be489eaf748f7d79ee7d8dd3f0177ab47626728f5b4dae9851b98e708f31e97c +size 32962 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a37f885bb6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb6369c1574670c81f2d0f5a541cc86da708cf087539250b50a1bb86fee33894 +size 33146 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f5c161a105 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72836a8215d294ccb10dd961c5bde18342f50ab4aedf23c6bd5e655edd733a4a +size 31348 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e78f91910a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86591b97276d7379ae68c500a81a977ee0adb2f8b28e0896d0b32a658fb359bf +size 25430 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..968d9fdff5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:110139dd6177e5e1bd126bcd60e4442394867c2e0a3656bee7f15cc5c58c6a6e +size 32584 From 5a7f77bc92c2179281531f55fc7be630976d88c8 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 19 Oct 2023 13:32:43 +0200 Subject: [PATCH 074/281] Extract more content from audio messages. (#1607) `TimelineItemAudioContent`: - Use `java.time.Duration` instead of milliseconds. This will ease up things in the future because currently milliseconds are sent over the wire but in the future seconds will be sent (as per the stable MSC). Using `Duration` will allow our downstream code to be independent of what's passed over the wire. - Rename `audioSource` property to `mediaSource` to better match its type. `AudioMessageType`: - Add and populate new fields `details` and `isVoiceMessage` to be used by voice messages. --- .../messages/impl/MessagesFlowNode.kt | 2 +- .../TimelineItemContentMessageFactory.kt | 7 +++-- .../model/event/TimelineItemAudioContent.kt | 5 ++-- .../event/TimelineItemAudioContentProvider.kt | 5 ++-- .../DefaultRoomLastMessageFormatterTest.kt | 2 +- .../matrix/api/media/AudioDetails.kt | 24 +++++++++++++++ .../api/timeline/item/event/MessageType.kt | 5 +++- .../matrix/impl/media/AudioDetails.kt | 30 +++++++++++++++++++ .../timeline/item/event/EventMessageMapper.kt | 8 ++++- 9 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index fcb2e7e5e8..21e384906e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -238,7 +238,7 @@ class MessagesFlowNode @AssistedInject constructor( backstack.push(navTarget) } is TimelineItemAudioContent -> { - val mediaSource = event.content.audioSource + val mediaSource = event.content.mediaSource val navTarget = NavTarget.MediaViewer( mediaInfo = MediaInfo( name = event.content.body, 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 ae2ea4f350..323f110f47 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 @@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import java.time.Duration import javax.inject.Inject class TimelineItemContentMessageFactory @Inject constructor( @@ -103,11 +104,11 @@ class TimelineItemContentMessageFactory @Inject constructor( } is AudioMessageType -> TimelineItemAudioContent( body = messageType.body, - audioSource = messageType.source, - duration = messageType.info?.duration?.toMillis() ?: 0L, + mediaSource = messageType.source, + duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), - fileExtension = fileExtensionExtractor.extractFromName(messageType.body) + fileExtension = fileExtensionExtractor.extractFromName(messageType.body), ) is FileMessageType -> { val fileExtension = fileExtensionExtractor.extractFromName(messageType.body) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt index 485b863170..9d9a41e0e3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt @@ -18,11 +18,12 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize import io.element.android.libraries.matrix.api.media.MediaSource +import java.time.Duration data class TimelineItemAudioContent( val body: String, - val duration: Long, - val audioSource: MediaSource, + val duration: Duration, + val mediaSource: MediaSource, val mimeType: String, val formattedFileSize: String, val fileExtension: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt index ed424781f8..06cb53b6fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource +import java.time.Duration open class TimelineItemAudioContentProvider : PreviewParameterProvider { override val values: Sequence @@ -34,6 +35,6 @@ fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAu mimeType = MimeTypes.Pdf, formattedFileSize = "100kB", fileExtension = "mp3", - duration = 100, - audioSource = MediaSource(""), + duration = Duration.ofMillis(100), + mediaSource = MediaSource(""), ) 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 4c26fcb3c7..50d313f132 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 @@ -161,7 +161,7 @@ class DefaultRoomLastMessageFormatterTest { val sharedContentMessagesTypes = arrayOf( TextMessageType(body, null), VideoMessageType(body, MediaSource("url"), null), - AudioMessageType(body, MediaSource("url"), null), + AudioMessageType(body, MediaSource("url"), null, null, false), ImageMessageType(body, MediaSource("url"), null), FileMessageType(body, MediaSource("url"), null), LocationMessageType(body, "geo:1,2", null), diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt new file mode 100644 index 0000000000..f8cd2d3fb4 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt @@ -0,0 +1,24 @@ +/* + * 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.api.media + +import java.time.Duration + +data class AudioDetails( + val duration: Duration, + val waveform: List, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt index dc06d5c94a..ba6eeca819 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event +import io.element.android.libraries.matrix.api.media.AudioDetails import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo @@ -46,7 +47,9 @@ data class LocationMessageType( data class AudioMessageType( val body: String, val source: MediaSource, - val info: AudioInfo? + val info: AudioInfo?, + val details: AudioDetails?, + val isVoiceMessage: Boolean, ) : MessageType data class VideoMessageType( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt new file mode 100644 index 0000000000..c3fa11e40c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt @@ -0,0 +1,30 @@ +/* + * 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.media + +import io.element.android.libraries.matrix.api.media.AudioDetails +import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent as RustAudioDetails + +fun RustAudioDetails.map(): AudioDetails = AudioDetails( + duration = duration, + waveform = waveform.map { it.toInt() }, +) + +fun AudioDetails.map(): RustAudioDetails = RustAudioDetails( + duration = duration, + waveform = waveform.map { it.toUShort() } +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 0a59cfddab..18d2e1bdeb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -75,7 +75,13 @@ class EventMessageMapper { fun mapMessageType(type: RustMessageType?) = when (type) { is RustMessageType.Audio -> { - AudioMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) + AudioMessageType( + body = type.content.body, + source = type.content.source.map(), + info = type.content.info?.map(), + details = type.content.audio?.map(), + isVoiceMessage = type.content.voice != null, + ) } is RustMessageType.File -> { FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) From 33f5c8efb8f1e495d447532b0b0ff6c17bee6099 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 19 Oct 2023 14:07:45 +0200 Subject: [PATCH 075/281] Add global `context.cacheDir` provider. (#1606) ## Type of change - [ ] Feature - [ ] Bugfix - [x] Technical - [ ] Other : ## Content Dagger now provides the app's `cacheDir` when requesting a `@CacheDirectory File` type. ## Motivation and context To support some upcoming code that needs the `cacheDir` to be changed during tests. --- .../io/element/android/x/di/AppModule.kt | 7 +++++ .../libraries/di/ApplicationContext.kt | 8 +++++- .../android/libraries/di/CacheDirectory.kt | 27 +++++++++++++++++++ .../matrix/impl/RustMatrixClientFactory.kt | 7 +++-- .../android/samples/minimal/MainActivity.kt | 2 +- 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index 17ba415762..037cec1e71 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.DefaultPreferences import io.element.android.libraries.di.SingleIn import io.element.android.x.BuildConfig @@ -51,6 +52,12 @@ object AppModule { return File(context.filesDir, "sessions") } + @Provides + @CacheDirectory + fun providesCacheDirectory(@ApplicationContext context: Context): File { + return context.cacheDir + } + @Provides fun providesResources(@ApplicationContext context: Context): Resources { return context.resources diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt index 2108678097..421b521192 100644 --- a/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt @@ -18,4 +18,10 @@ package io.element.android.libraries.di import javax.inject.Qualifier -@Qualifier annotation class ApplicationContext +/** + * Qualifies a [Context] object that represents the application context. + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Qualifier +annotation class ApplicationContext diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt new file mode 100644 index 0000000000..e8513bef45 --- /dev/null +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt @@ -0,0 +1,27 @@ +/* + * 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.di + +import javax.inject.Qualifier + +/** + * Qualifies a [File] object which represents the application cache directory. + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Qualifier +annotation class CacheDirectory diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index b37266342e..b1cb3ffac4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -16,9 +16,8 @@ package io.element.android.libraries.matrix.impl -import android.content.Context import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -32,8 +31,8 @@ import java.io.File import javax.inject.Inject class RustMatrixClientFactory @Inject constructor( - @ApplicationContext private val context: Context, private val baseDirectory: File, + @CacheDirectory private val cacheDirectory: File, private val appCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, private val sessionStore: SessionStore, @@ -63,7 +62,7 @@ class RustMatrixClientFactory @Inject constructor( appCoroutineScope = appCoroutineScope, dispatchers = coroutineDispatchers, baseDirectory = baseDirectory, - baseCacheDirectory = context.cacheDir, + baseCacheDirectory = cacheDirectory, clock = clock, ) } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index c95a44d8ea..a405167cbf 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -49,8 +49,8 @@ class MainActivity : ComponentActivity() { sessionStore = sessionStore, userAgentProvider = userAgentProvider, rustMatrixClientFactory = RustMatrixClientFactory( - context = applicationContext, baseDirectory = baseDirectory, + cacheDirectory = applicationContext.cacheDir, appCoroutineScope = Singleton.appScope, coroutineDispatchers = Singleton.coroutineDispatchers, sessionStore = sessionStore, From e88a5fc8587b9e73840540fa2f75167c463daa43 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 15:45:11 +0200 Subject: [PATCH 076/281] Pin create: add test for presenter --- build.gradle.kts | 1 - features/lockscreen/impl/build.gradle.kts | 1 + .../impl/create/CreatePinPresenter.kt | 6 +- .../impl/create/CreatePinStateProvider.kt | 4 +- .../lockscreen/impl/create/CreatePinView.kt | 8 +- .../create/validation/CreatePinFailure.kt | 4 +- .../impl/create/validation/PinValidator.kt | 10 +- .../impl/create/CreatePinPresenterTest.kt | 113 ++++++++++++++++++ .../android/tests/testutils/ReceiveTurbine.kt | 10 ++ 9 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 487776d948..e14ad71981 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -253,7 +253,6 @@ koverMerged { // Temporary until we have actually something to test. excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter" excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*" - excludes += "io.element.android.features.lockscreen.impl.create.CreatePinPresenter" } bound { minValue = 85 diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index af63538db5..028d8bee3c 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index 525b80314b..e72e636ed4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -57,7 +57,7 @@ class CreatePinPresenter @Inject constructor( if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { - createPinFailure = CreatePinFailure.ConfirmationPinNotMatching + createPinFailure = CreatePinFailure.PinsDontMatch } } } else { @@ -74,11 +74,11 @@ class CreatePinPresenter @Inject constructor( } CreatePinEvents.ClearFailure -> { when (createPinFailure) { - is CreatePinFailure.ConfirmationPinNotMatching -> { + is CreatePinFailure.PinsDontMatch -> { choosePinEntry = PinEntry.empty(PIN_SIZE) confirmPinEntry = PinEntry.empty(PIN_SIZE) } - is CreatePinFailure.ChosenPinBlacklisted -> { + is CreatePinFailure.PinBlacklisted -> { choosePinEntry = PinEntry.empty(PIN_SIZE) } null -> Unit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index 40287622fd..543360f91e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -35,11 +35,11 @@ open class CreatePinStateProvider : PreviewParameterProvider { choosePinEntry = PinEntry.empty(4).fillWith("1789"), confirmPinEntry = PinEntry.empty(4).fillWith("1788"), isConfirmationStep = true, - creationFailure = CreatePinFailure.ConfirmationPinNotMatching + creationFailure = CreatePinFailure.PinsDontMatch ), aCreatePinState( choosePinEntry = PinEntry.empty(4).fillWith("1111"), - creationFailure = CreatePinFailure.ChosenPinBlacklisted + creationFailure = CreatePinFailure.PinBlacklisted ), ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index fdce08c229..915bd2b4b0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -125,16 +125,16 @@ private fun CreatePinContent( @Composable private fun CreatePinFailure.content(): String { return when (this) { - CreatePinFailure.ChosenPinBlacklisted -> "You cannot choose this as your PIN code for security reasons" - CreatePinFailure.ConfirmationPinNotMatching -> "Please enter the same PIN twice" + CreatePinFailure.PinBlacklisted -> "You cannot choose this as your PIN code for security reasons" + CreatePinFailure.PinsDontMatch -> "Please enter the same PIN twice" } } @Composable private fun CreatePinFailure.title(): String { return when (this) { - CreatePinFailure.ChosenPinBlacklisted -> "Choose a different PIN" - CreatePinFailure.ConfirmationPinNotMatching -> "PINs don't match" + CreatePinFailure.PinBlacklisted -> "Choose a different PIN" + CreatePinFailure.PinsDontMatch -> "PINs don't match" } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt index 96c0de0056..8c0cb78921 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt @@ -17,6 +17,6 @@ package io.element.android.features.lockscreen.impl.create.validation sealed interface CreatePinFailure { - data object ChosenPinBlacklisted : CreatePinFailure - data object ConfirmationPinNotMatching : CreatePinFailure + data object PinBlacklisted : CreatePinFailure + data object PinsDontMatch : CreatePinFailure } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt index 8c1854ecee..7353ec47d0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt @@ -16,13 +16,17 @@ package io.element.android.features.lockscreen.impl.create.validation +import androidx.annotation.VisibleForTesting import io.element.android.features.lockscreen.impl.create.model.PinEntry import javax.inject.Inject -private val BLACKLIST = listOf("0000", "1234") - class PinValidator @Inject constructor() { + companion object { + @VisibleForTesting + val BLACKLIST = listOf("0000", "1234") + } + sealed interface Result { data object Valid : Result data class Invalid(val failure: CreatePinFailure) : Result @@ -32,7 +36,7 @@ class PinValidator @Inject constructor() { val pinAsText = pinEntry.toText() val isBlacklisted = BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { - Result.Invalid(CreatePinFailure.ChosenPinBlacklisted) + Result.Invalid(CreatePinFailure.PinBlacklisted) } else { Result.Valid } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt new file mode 100644 index 0000000000..9c86039fe1 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt @@ -0,0 +1,113 @@ +/* + * 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.lockscreen.impl.create + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.create.model.PinDigit +import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CreatePinPresenterTest { + + private val blacklistedPin = PinValidator.BLACKLIST.first() + private val halfCompletePin = "12" + private val completePin = "1235" + private val mismatchedPin = "1236" + + @Test + fun `present - complete flow`() = runTest { + + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().also { state -> + state.choosePinEntry.assertEmpty() + state.confirmPinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + assertThat(state.isConfirmationStep).isFalse() + state.eventSink(CreatePinEvents.OnPinEntryChanged(halfCompletePin)) + } + awaitItem().also { state -> + state.choosePinEntry.assertText(halfCompletePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + assertThat(state.isConfirmationStep).isFalse() + state.eventSink(CreatePinEvents.OnPinEntryChanged(blacklistedPin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(blacklistedPin) + assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinBlacklisted) + state.eventSink(CreatePinEvents.ClearFailure) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertEmpty() + assertThat(state.createPinFailure).isNull() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isTrue() + state.eventSink(CreatePinEvents.OnPinEntryChanged(mismatchedPin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertText(mismatchedPin) + assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinsDontMatch) + state.eventSink(CreatePinEvents.ClearFailure) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertEmpty() + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isFalse() + assertThat(state.createPinFailure).isNull() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitLastSequentialItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertEmpty() + assertThat(state.isConfirmationStep).isTrue() + state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + } + awaitItem().also { state -> + state.choosePinEntry.assertText(completePin) + state.confirmPinEntry.assertText(completePin) + } + } + } + + private fun PinEntry.assertText(text: String) { + assertThat(toText()).isEqualTo(text) + } + + private fun PinEntry.assertEmpty() { + val isEmpty = digits.all { it is PinDigit.Empty } + assertThat(isEmpty).isTrue() + } + + private fun createPresenter(): CreatePinPresenter { + return CreatePinPresenter(PinValidator()) + } +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt index 06b6b3d3ea..3e47dd63ce 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt @@ -32,6 +32,16 @@ suspend fun ReceiveTurbine.consumeItemsUntilTimeout(timeout: Durati return consumeItemsUntilPredicate(timeout) { false } } +/** + * Consume all items which are emitted sequentially. + * Use the smallest timeout possible internally to avoid wasting time. + * Same as calling skipItems(x) and then awaitItem() but without assumption on the number of items. + * @return the last item emitted. + */ +suspend fun ReceiveTurbine.awaitLastSequentialItem(): T { + return consumeItemsUntilTimeout(1.milliseconds).last() +} + /** * Consume items until predicate is true, or timeout is reached waiting for an event, or we receive terminal event. * The timeout is applied for each event. From a814c4a95ab3cd1148c3cb35cf83eca040ce6c90 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 19 Oct 2023 15:57:34 +0200 Subject: [PATCH 077/281] TimelineItemPresenterFactories (#1609) DI infrastructure to allow injection of presenters into the timeline. Add an `@AssistedFactory` of type `TimelineItemPresenterFactory` to a `Presenter` class and bind this factory into the TimelineItemPresenterFactory map multi binding using: ``` @Binds @IntoMap @TimelineItemEventContentKey(MyTimelineItemContent::class) ``` A map multibinding of such factories will be available in the `LocalTimelineItemPresenterFactories` composition local for further use down the UI tree. --- .../features/messages/impl/MessagesNode.kt | 32 +++++--- .../di/TimelineItemEventContentKey.kt | 29 +++++++ .../di/TimelineItemPresenterFactories.kt | 77 +++++++++++++++++++ .../di/TimelineItemPresenterFactory.kt | 33 ++++++++ tools/detekt/detekt.yml | 2 +- 5 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt 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 6a3cf502d7..dbf7e2fbb2 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 @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -28,6 +29,8 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -44,6 +47,7 @@ class MessagesNode @AssistedInject constructor( private val room: MatrixRoom, private val analyticsService: AnalyticsService, private val presenterFactory: MessagesPresenter.Factory, + private val timelineItemPresenterFactories: TimelineItemPresenterFactories, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create(this) @@ -106,17 +110,21 @@ class MessagesNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val state = presenter.present() - MessagesView( - state = state, - onBackPressed = this::navigateUp, - onRoomDetailsClicked = this::onRoomDetailsClicked, - onEventClicked = this::onEventClicked, - onPreviewAttachments = this::onPreviewAttachments, - onUserDataClicked = this::onUserDataClicked, - onSendLocationClicked = this::onSendLocationClicked, - onCreatePollClicked = this::onCreatePollClicked, - modifier = modifier, - ) + CompositionLocalProvider( + LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, + ) { + val state = presenter.present() + MessagesView( + state = state, + onBackPressed = this::navigateUp, + onRoomDetailsClicked = this::onRoomDetailsClicked, + onEventClicked = this::onEventClicked, + onPreviewAttachments = this::onPreviewAttachments, + onUserDataClicked = this::onUserDataClicked, + onSendLocationClicked = this::onSendLocationClicked, + onCreatePollClicked = this::onCreatePollClicked, + modifier = modifier, + ) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt new file mode 100644 index 0000000000..9cb046a054 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt @@ -0,0 +1,29 @@ +/* + * 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.di + +import dagger.MapKey +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import kotlin.reflect.KClass + +/** + * Annotation to add a factory of type [TimelineItemPresenterFactory] to a + * Dagger map multi binding keyed with a subclass of [TimelineItemEventContent]. + */ +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class TimelineItemEventContentKey(val value: KClass) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt new file mode 100644 index 0000000000..0574f7e903 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt @@ -0,0 +1,77 @@ +/* + * 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.di + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.multibindings.Multibinds +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.RoomScope +import javax.inject.Inject + +/** + * Dagger module that declares the [TimelineItemPresenterFactory] map multi binding. + * + * Its sole purpose is to support the case of an empty map multibinding. + */ +@Module +@ContributesTo(RoomScope::class) +interface TimelineItemPresenterFactoriesModule { + @Multibinds + fun multiBindTimelineItemPresenterFactories(): @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>> +} + +/** + * Wrapper around the [TimelineItemPresenterFactory] map multi binding. + * + * Its only purpose is to provide a nicer type name than: + * `@JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>>`. + * + * A typealias would have been better but typealiases on Dagger types which use @JvmSuppressWildcards + * currently make Dagger crash. + * + * Request this type from Dagger to access the [TimelineItemPresenterFactory] map multibinding. + */ +data class TimelineItemPresenterFactories @Inject constructor( + val factories: @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>>, +) + +/** + * Provides a [TimelineItemPresenterFactories] to the composition. + */ +val LocalTimelineItemPresenterFactories = staticCompositionLocalOf { + TimelineItemPresenterFactories(emptyMap()) +} + +/** + * Creates and remembers a presenter for the given content. + * + * Will throw if the presenter is not found in the [TimelineItemPresenterFactory] map multi binding. + */ +@Composable +inline fun TimelineItemPresenterFactories.rememberPresenter( + content: C +): Presenter = remember(content) { + factories.getValue(C::class.java).let { + @Suppress("UNCHECKED_CAST") + (it as TimelineItemPresenterFactory).create(content) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt new file mode 100644 index 0000000000..f79d606f60 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.di + +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.libraries.architecture.Presenter + +/** + * A factory for a [Presenter] associated with a timeline item. + * + * Implementations should be annotated with [AssistedFactory] to be created by Dagger. + * + * @param C The timeline item's [TimelineItemEventContent] subtype. + * @param S The [Presenter]'s state class. + * @return A [Presenter] that produces a state of type [S] for the given content of type [C]. + */ +fun interface TimelineItemPresenterFactory { + fun create(content: C): Presenter +} diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 1c226dd5ed..ca8370ae23 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -219,7 +219,7 @@ Compose: CompositionLocalAllowlist: active: true # You can optionally define a list of CompositionLocals that are allowed here - allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState + allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState, LocalTimelineItemPresenterFactories CompositionLocalNaming: active: true ContentEmitterReturningValues: From 7ebe6719dac0dee7e94d86b9a8c42e572f280a95 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 16:28:20 +0200 Subject: [PATCH 078/281] Pin create: use localazy strings --- .../lockscreen/impl/create/CreatePinState.kt | 1 + .../lockscreen/impl/create/CreatePinView.kt | 42 +++++++++++---- .../lockscreen/impl/create/model/PinEntry.kt | 2 +- .../impl/src/main/res/values/localazy.xml | 24 +++++++++ .../api/src/main/res/values/localazy.xml | 4 ++ .../src/main/res/values/localazy.xml | 54 ++++++++++++++++++- tools/localazy/config.json | 6 +++ 7 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 features/lockscreen/impl/src/main/res/values/localazy.xml diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 914e12ca96..5bb632f04e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -26,6 +26,7 @@ data class CreatePinState( val createPinFailure: CreatePinFailure?, val eventSink: (CreatePinEvents) -> Unit ) { + val pinSize = choosePinEntry.size val activePinEntry = if (isConfirmationStep) { confirmPinEntry } else { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 915bd2b4b0..dc9956abaa 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.background 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.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth @@ -33,13 +34,17 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure @@ -76,7 +81,7 @@ fun CreatePinView( modifier = Modifier .padding(padding) .consumeWindowInsets(padding), - header = { CreatePinHeader(state.isConfirmationStep) }, + header = { CreatePinHeader(state.isConfirmationStep, state.pinSize) }, content = { CreatePinContent(state) } ) } @@ -86,14 +91,31 @@ fun CreatePinView( @Composable private fun CreatePinHeader( isValidationStep: Boolean, + pinSize: Int, modifier: Modifier = Modifier, ) { - IconTitleSubtitleMolecule( + Column( modifier = modifier, - title = if (isValidationStep) "Confirm PIN" else "Choose 4 digit PIN", - subTitle = "Lock Element to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app", - iconImageVector = Icons.Default.Lock, - ) + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), + title = if (isValidationStep) { + stringResource(id = R.string.screen_app_lock_setup_confirm_pin) + } else { + stringResource(id = R.string.screen_app_lock_setup_choose_pin, pinSize) + }, + subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context), + iconImageVector = Icons.Filled.Lock, + ) + Text( + text = stringResource(id = R.string.screen_app_lock_setup_pin_context_warning), + modifier = Modifier.padding(8.dp), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.secondary, + ) + } } @Composable @@ -125,16 +147,16 @@ private fun CreatePinContent( @Composable private fun CreatePinFailure.content(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> "You cannot choose this as your PIN code for security reasons" - CreatePinFailure.PinsDontMatch -> "Please enter the same PIN twice" + CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_content) + CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content) } } @Composable private fun CreatePinFailure.title(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> "Choose a different PIN" - CreatePinFailure.PinsDontMatch -> "PINs don't match" + CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_title) + CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt index 2228110156..a97315f2e8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt @@ -32,7 +32,7 @@ data class PinEntry( } } - private val size = digits.size + val size = digits.size /** * Fill the first digits with the given text. diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..fb5c2be73c --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -0,0 +1,24 @@ + + + + "Wrong PIN. You have %1$d more chance" + "Wrong PIN. You have %1$d more chances" + + "Forgot PIN?" + "Change PIN code" + "Allow biometric unlock" + "Remove PIN" + "Are you sure you want to remove PIN?" + "Remove PIN?" + "Choose %1$d digit PIN" + "Confirm PIN" + "You cannot choose this as your PIN code for security reasons" + "Choose a different PIN" + "Lock Element to add extra security to your chats." + "Choose something memorable. If you forget this PIN, you will be logged out of the app." + "Please enter the same PIN twice" + "PINs don\'t match" + "You’ll need to re-login and create a new PIN to proceed" + "You are being signed out" + "You have 3 attempts to unlock" + diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index 9ea4bb77fd..5a5c9c64ca 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -1,8 +1,12 @@ + "Please wait for this to complete before signing out." + "Your keys are still being backed up" "Are you sure you want to sign out?" "Sign out" "Signing out…" + "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." + "Have you saved your recovery key?" "Sign out" "Sign out" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index b9a6460bec..c7bd2f2ca9 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -5,6 +5,7 @@ "Mentions only" "Muted" "Pause" + "PIN field" "Play" "Poll" "Ended poll" @@ -69,6 +70,8 @@ "Share" "Share link" "Sign in again" + "Sign out" + "Sign out anyway" "Skip" "Start" "Start chat" @@ -84,6 +87,7 @@ "Analytics" "Audio" "Bubbles" + "Chat backup" "Copyright" "Creating room…" "Left room" @@ -93,6 +97,7 @@ "Editing" "* %1$s %2$s" "Encryption enabled" + "Enter your PIN" "Error" "Everyone" "File" @@ -122,6 +127,7 @@ "Privacy policy" "Reaction" "Reactions" + "Recovery key" "Refreshing…" "Replying to %1$s" "Report a bug" @@ -129,9 +135,9 @@ "Rich text editor" "Room name" "e.g. your project name" + "Screen lock" "Search for someone" "Search results" - "Secure backup" "Security" "Sending…" "Server not supported" @@ -151,6 +157,7 @@ "Unable to decrypt" "Invites couldn\'t be sent to one or more users." "Unable to send invite(s)" + "Unlock" "Unmute" "Unsupported event" "Username" @@ -177,6 +184,7 @@ "%1$s could not access your location. Please try again later." "%1$s does not have permission to access your location. You can enable access in Settings." "%1$s does not have permission to access your location. Enable access below." + "%1$s does not have permission to access your microphone. Enable access to record a voice message." "Some messages have not been sent" "Sorry, an error occurred" "🔐️ Join me on %1$s" @@ -185,6 +193,10 @@ "Are you sure that you want to leave this room? This room is not public and you won\'t be able to rejoin without an invite." "Are you sure that you want to leave the room?" "%1$s Android" + + "%1$d digit entered" + "%1$d digits entered" + "%1$d member" "%1$d members" @@ -199,7 +211,26 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "Custom Element Call base URL" + "Set a custom base URL for Element Call." + "Invalid URL, please make sure you include the protocol (http/https) and the correct address." "Share analytics data" + "Turn off backup" + "Turn on backup" + "Backup ensures that you don\'t lose your message history. %1$s." + "Backup" + "Change recovery key" + "Confirm recovery key" + "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." + "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?" + "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:" + "Not have encrypted message history on new devices" + "Lose access to your encrypted messages if you are signed out of %1$s everywhere" + "Are you sure you want to turn off backup?" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." @@ -228,6 +259,27 @@ If you proceed, some of your settings may change." "system settings" "System notifications turned off" "Notifications" + "Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work." + "Generate a new recovery key" + "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." + "Enter the 48 character code." + "Enter…" + "Recovery key confirmed" + "Confirm your recovery key" + "Save recovery key" + "Write down your recovery key somewhere safe or save it in a password manager." + "Tap to copy recovery key" + "Save your recovery key" + "You will not be able to access your new recovery key after this step." + "Have you saved your recovery key?" + "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’." + "Generate your recovery key" + "Make sure you can store your recovery key somewhere safe" + "Recovery setup successful" + "Set up recovery" "Check if you want to hide all current and future messages from this user" "Share location" "Share my location" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 7e07d269c0..fd35feb36e 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -153,6 +153,12 @@ "includeRegex": [ "call_.*" ] + }, + { + "name": ":features:lockscreen:impl", + "includeRegex": [ + "screen_app_lock_.*" + ] } ] } From 55282417a87a7b6dd56f3043df2e06ec8ca71bdb Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 16:29:16 +0200 Subject: [PATCH 079/281] Create pin : change digit size box --- .../android/features/lockscreen/impl/create/CreatePinView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index dc9956abaa..fa79016472 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -211,7 +211,7 @@ private fun PinDigitView( } Box( modifier = modifier - .size(40.dp, 50.dp) + .size(48.dp) .then(appearanceModifier), contentAlignment = Alignment.Center, From b5a9dc80ddfc7ef600a41daa7116c8b99719e5bf Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 16:32:58 +0200 Subject: [PATCH 080/281] Create pin : fix konsist --- .../android/features/lockscreen/impl/create/CreatePinView.kt | 2 +- .../features/lockscreen/impl/create/CreatePinPresenterTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index fa79016472..4e01de8c2f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -161,7 +161,7 @@ private fun CreatePinFailure.title(): String { } @Composable -fun PinEntryTextField( +private fun PinEntryTextField( pinEntry: PinEntry, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt index 9c86039fe1..c1af14f519 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt @@ -38,7 +38,7 @@ class CreatePinPresenterTest { @Test fun `present - complete flow`() = runTest { - val presenter = createPresenter() + val presenter = createCreatePinPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -107,7 +107,7 @@ class CreatePinPresenterTest { assertThat(isEmpty).isTrue() } - private fun createPresenter(): CreatePinPresenter { + private fun createCreatePinPresenter(): CreatePinPresenter { return CreatePinPresenter(PinValidator()) } } From 7505ac8edac1c8452efdce70747621f610dd2189 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 19 Oct 2023 16:17:57 +0100 Subject: [PATCH 081/281] Fix tests and lint --- ...efaultNotificationSettingStateProvider.kt} | 2 +- .../EditDefaultNotificationSettingView.kt | 2 +- ...faultNotificationSettingsPresenterTests.kt | 8 +-- .../RoomNotificationSettingsOptions.kt | 43 ++++++++++++++ .../RoomNotificationSettingsView.kt | 33 +---------- .../ShowChangeNotificationSettingError.kt | 31 ++++++++++ ...edRoomNotificationSettingsStateProvider.kt | 42 +++++++++++++ ...UserDefinedRoomNotificationSettingsView.kt | 13 +++- .../RoomNotificationSettingsPresenterTests.kt | 59 +++++++++++++------ .../FakeNotificationSettingsService.kt | 18 ++++-- 10 files changed, 190 insertions(+), 61 deletions(-) rename features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/{EditDefaultNotificationSettingsStateProvider.kt => EditDefaultNotificationSettingStateProvider.kt} (93%) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOptions.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/ShowChangeNotificationSettingError.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt similarity index 93% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt index 6910b581bb..3446c24efe 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails -open class EditDefaultNotificationSettingsStateProvider: PreviewParameterProvider { +open class EditDefaultNotificationSettingStateProvider: PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anEditDefaultNotificationSettingsState(), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt index bf82de7251..94f5a6b053 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt @@ -136,7 +136,7 @@ fun EditDefaultNotificationSettingView( @PreviewsDayNight @Composable internal fun EditDefaultNotificationSettingViewPreview( - @PreviewParameter(EditDefaultNotificationSettingsStateProvider::class) state: EditDefaultNotificationSettingState + @PreviewParameter(EditDefaultNotificationSettingStateProvider::class) state: EditDefaultNotificationSettingState ) = ElementPreview { EditDefaultNotificationSettingView( state = state, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt index 8376aa95c3..2269df46e1 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTests.kt @@ -38,7 +38,7 @@ class EditDefaultNotificationSettingsPresenterTests { @Test fun `present - ensures initial state is correct`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - val presenter = createPresenter(notificationSettingsService) + val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -64,7 +64,7 @@ class EditDefaultNotificationSettingsPresenterTests { givenGetRoomResult(A_ROOM_ID, room) } val roomListService = FakeRoomListService() - val presenter = createPresenter(notificationSettingsService, roomListService, matrixClient) + val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService, matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -78,7 +78,7 @@ class EditDefaultNotificationSettingsPresenterTests { @Test fun `present - edit default notification setting`() = runTest { - val presenter = createPresenter() + val presenter = createEditDefaultNotificationSettingPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -90,7 +90,7 @@ class EditDefaultNotificationSettingsPresenterTests { } } - private fun createPresenter( + private fun createEditDefaultNotificationSettingPresenter( notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), roomListService: FakeRoomListService = FakeRoomListService(), matrixClient: FakeMatrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOptions.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOptions.kt new file mode 100644 index 0000000000..878632db9c --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsOptions.kt @@ -0,0 +1,43 @@ +/* + * 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.roomdetails.impl.notificationsettings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.libraries.matrix.api.room.RoomNotificationMode + +@Composable +fun RoomNotificationSettingsOptions( + selected: RoomNotificationMode?, + enabled: Boolean, + modifier: Modifier = Modifier, + onOptionSelected: (RoomNotificationSettingsItem) -> Unit = {}, +) { + val items = roomNotificationSettingsItems() + Column(modifier = modifier.selectableGroup()) { + items.forEach { item -> + RoomNotificationSettingsOption( + roomNotificationSettingsItem = item, + isSelected = selected == item.mode, + onOptionSelected = onOptionSelected, + enabled = enabled + ) + } + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt index b0c82d291c..3d95e4ec20 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -33,12 +32,11 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceText -import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.Scaffold import io.element.android.libraries.designsystem.theme.components.Text @@ -146,35 +144,6 @@ private fun RoomNotificationSettingsTopBar( ) } -@Composable -fun RoomNotificationSettingsOptions( - selected: RoomNotificationMode?, - enabled: Boolean, - modifier: Modifier = Modifier, - onOptionSelected: (RoomNotificationSettingsItem) -> Unit = {}, -) { - val items = roomNotificationSettingsItems() - Column(modifier = modifier.selectableGroup()) { - items.forEach { item -> - RoomNotificationSettingsOption( - roomNotificationSettingsItem = item, - isSelected = selected == item.mode, - onOptionSelected = onOptionSelected, - enabled = enabled - ) - } - } -} - -@Composable -fun ShowChangeNotificationSettingError(state: RoomNotificationSettingsState, event: RoomNotificationSettingsEvents) { - ErrorDialog( - title = stringResource(CommonStrings.dialog_title_error), - content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode), - onDismiss = { state.eventSink(event) }, - ) -} - @PreviewsDayNight @Composable internal fun RoomNotificationSettingsPreview( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/ShowChangeNotificationSettingError.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/ShowChangeNotificationSettingError.kt new file mode 100644 index 0000000000..4b99976988 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/ShowChangeNotificationSettingError.kt @@ -0,0 +1,31 @@ +/* + * 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.roomdetails.impl.notificationsettings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun ShowChangeNotificationSettingError(state: RoomNotificationSettingsState, event: RoomNotificationSettingsEvents) { + ErrorDialog( + title = stringResource(CommonStrings.dialog_title_error), + content = stringResource(CommonStrings.screen_notification_settings_edit_failed_updating_default_mode), + onDismiss = { state.eventSink(event) }, + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt new file mode 100644 index 0000000000..76714a82d0 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.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.roomdetails.impl.notificationsettings + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.RoomNotificationSettings + +internal class UserDefinedRoomNotificationSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + RoomNotificationSettingsState( + roomName = "Room 1", + Async.Success( + RoomNotificationSettings( + mode = RoomNotificationMode.MUTE, + isDefault = false) + ), + pendingRoomNotificationMode = null, + pendingSetDefault = null, + defaultRoomNotificationMode = RoomNotificationMode.ALL_MESSAGES, + setNotificationSettingAction = Async.Uninitialized, + restoreDefaultAction = Async.Uninitialized, + eventSink = { }, + ), + ) +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt index 75642f4e15..6afde5bbf9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.architecture.Async @@ -36,6 +37,8 @@ import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.preferences.PreferenceText +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 @@ -113,7 +116,7 @@ fun UserDefinedRoomNotificationSettingsView( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun UserDefinedRoomNotificationSettingsTopBar( +private fun UserDefinedRoomNotificationSettingsTopBar( roomName: String, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, @@ -128,3 +131,11 @@ fun UserDefinedRoomNotificationSettingsTopBar( navigationIcon = { BackButton(onClick = onBackPressed) }, ) } + +@PreviewsDayNight +@Composable +internal fun UserDefinedRoomNotificationSettingsPreview( + @PreviewParameter(UserDefinedRoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState +) = ElementPreview { + UserDefinedRoomNotificationSettingsView(state) +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt index 7d4c83d518..7f4ea57cf1 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/notificationsettings/RoomNotificationSettingsPresenterTests.kt @@ -25,6 +25,7 @@ import io.element.android.features.roomdetails.impl.notificationsettings.RoomNot import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsPresenter import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.test.runTest @@ -34,12 +35,12 @@ import kotlin.time.Duration.Companion.milliseconds class RoomNotificationSettingsPresenterTests { @Test fun `present - initial state is created from room info`() = runTest { - val presenter = aNotificationPresenter() + val presenter = createRoomNotificationSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - Truth.assertThat(initialState.roomNotificationSettings).isNull() + Truth.assertThat(initialState.roomNotificationSettings.dataOrNull()).isNull() Truth.assertThat(initialState.defaultRoomNotificationMode).isNull() cancelAndIgnoreRemainingEvents() } @@ -47,53 +48,74 @@ class RoomNotificationSettingsPresenterTests { @Test fun `present - notification mode changed`() = runTest { - val presenter = aNotificationPresenter() + val presenter = createRoomNotificationSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)) val updatedState = consumeItemsUntilPredicate { - it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + it.roomNotificationSettings.dataOrNull()?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }.last() - Truth.assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + Truth.assertThat(updatedState.roomNotificationSettings.dataOrNull()?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + cancelAndIgnoreRemainingEvents() } } @Test fun `present - observe notification mode changed`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - val presenter = aNotificationPresenter(notificationSettingsService) + val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { notificationSettingsService.setRoomNotificationMode(A_ROOM_ID, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) - val updatedState = consumeItemsUntilPredicate() { - it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + val updatedState = consumeItemsUntilPredicate { + it.roomNotificationSettings.dataOrNull()?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }.last() - Truth.assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + Truth.assertThat(updatedState.roomNotificationSettings.dataOrNull()?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) } } @Test - fun `present - notification settings set custom`() = runTest { + fun `present - notification settings set custom failed`() = runTest { val notificationSettingsService = FakeNotificationSettingsService() - val presenter = aNotificationPresenter(notificationSettingsService) + notificationSettingsService.givenSetNotificationModeError(A_THROWABLE) + val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(false)) - val defaultState = consumeItemsUntilPredicate(timeout = 8000.milliseconds) { - it.roomNotificationSettings?.isDefault == false + val states = consumeItemsUntilPredicate { + it.roomNotificationSettings.dataOrNull()?.isDefault == false + } + states.forEach { + Truth.assertThat(it.roomNotificationSettings.dataOrNull()?.isDefault).isTrue() + Truth.assertThat(it.pendingSetDefault).isNull() + } + } + } + + @Test + fun `present - notification settings set custom`() = runTest { + val notificationSettingsService = FakeNotificationSettingsService() + val presenter = createRoomNotificationSettingsPresenter(notificationSettingsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(false)) + val defaultState = consumeItemsUntilPredicate { + it.roomNotificationSettings.dataOrNull()?.isDefault == false }.last() - Truth.assertThat(defaultState.roomNotificationSettings?.isDefault).isFalse() + Truth.assertThat(defaultState.roomNotificationSettings.dataOrNull()?.isDefault).isFalse() } } @Test fun `present - notification settings restore default`() = runTest { - val presenter = aNotificationPresenter() + val presenter = createRoomNotificationSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -101,13 +123,14 @@ class RoomNotificationSettingsPresenterTests { initialState.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY)) initialState.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(true)) val defaultState = consumeItemsUntilPredicate(timeout = 2000.milliseconds) { - it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + it.roomNotificationSettings.dataOrNull()?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }.last() - Truth.assertThat(defaultState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + Truth.assertThat(defaultState.roomNotificationSettings.dataOrNull()?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) + cancelAndIgnoreRemainingEvents() } } - private fun aNotificationPresenter( + private fun createRoomNotificationSettingsPresenter( notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() ): RoomNotificationSettingsPresenter{ val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt index 2b65857053..7d7fb1a36a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notificationsettings/FakeNotificationSettingsService.kt @@ -42,6 +42,7 @@ class FakeNotificationSettingsService( private var roomNotificationModeIsDefault: Boolean = initialRoomModeIsDefault private var callNotificationsEnabled = false private var atRoomNotificationsEnabled = false + private var setNotificationModeError: Throwable? = null override val notificationSettingsChangeFlow: SharedFlow get() = _notificationSettingsStateFlow @@ -89,10 +90,15 @@ class FakeNotificationSettingsService( } override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result { - roomNotificationModeIsDefault = false - roomNotificationMode = mode - _notificationSettingsStateFlow.emit(Unit) - return Result.success(Unit) + val error = setNotificationModeError + return if (error != null) { + Result.failure(error) + } else { + roomNotificationModeIsDefault = false + roomNotificationMode = mode + _notificationSettingsStateFlow.emit(Unit) + Result.success(Unit) + } } override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result { @@ -131,4 +137,8 @@ class FakeNotificationSettingsService( override suspend fun getRoomsWithUserDefinedRules(): Result> { return Result.success(if (roomNotificationModeIsDefault) listOf() else listOf(A_ROOM_ID.value)) } + + fun givenSetNotificationModeError(throwable: Throwable?) { + setNotificationModeError = throwable + } } From 0b295b7f2a45b6e3571ca28d563f9023c555510b Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 19 Oct 2023 15:24:30 +0000 Subject: [PATCH 082/281] Update screenshots --- ...create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png index 03b059496a..f110f617f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df5f2cc45255cec07dc99de8df0f2e8dd06fdc3afded3ba43c8deec2bb7c1d0b -size 34529 +oid sha256:4e2d58ea747cf87fd6e4f70b3ebb1f1e638a4ec3f7ac8cb712566a889167c3e1 +size 35045 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png index ffcb20390e..e5a3fe28af 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c5f338938adeeb280be0b68f57b5ba0b4c9a904e8660e99fb6e57cd6b4774fc -size 34534 +oid sha256:4be560206e4fc9101d8a793ad5f5f9cc2057fb47e60f37f91ce6dae8e2b995e2 +size 35147 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png index 5e82145176..87cefa723f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a412e9c38549341f707e9a6379e165d554294c0f452fec82ab783fa9e0533ad7 -size 32374 +oid sha256:da83f8b634c8217636b08792498704d9ffc1623c8d4aedfb15265d2f5e085e86 +size 32805 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png index 2fdd3802d4..d3d3ba4038 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fbba51aa681041018dd41f9f2e69159e229411b2cf438a62367a7f1edbb857a -size 28781 +oid sha256:598214e07c2545ac4dd061f6237a737e91e9c47eeea01a63ab064b3398e39b68 +size 24046 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png index 26305ae91e..7476004b1f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:843141e92c23f9cbba98296fc543c365d33a708daaabe0ff9549ccb8d6f69af4 -size 36969 +oid sha256:0ec4a6f05bfb15e018c19205cb919f6619440e25392ed0a74fb2896ddb1decb6 +size 28266 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png index c3a42144c6..41a349426e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be489eaf748f7d79ee7d8dd3f0177ab47626728f5b4dae9851b98e708f31e97c -size 32962 +oid sha256:b7c84953e8c1c3b2384078be4ca8cd5c763c032bfa22bb2ba034dcbee8ef9c72 +size 33594 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png index a37f885bb6..8f6c3fdbba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb6369c1574670c81f2d0f5a541cc86da708cf087539250b50a1bb86fee33894 -size 33146 +oid sha256:0917206ffe964311097f05de3fb838e0cc23f6fdbb2cd106d3015a751b378cfc +size 33864 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png index f5c161a105..64bc9a11cd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72836a8215d294ccb10dd961c5bde18342f50ab4aedf23c6bd5e655edd733a4a -size 31348 +oid sha256:32b2c6514d9a23da09c932e198f29ec112b5afa872a0fa42cd5de85689660891 +size 31658 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png index e78f91910a..6d10aeb87d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86591b97276d7379ae68c500a81a977ee0adb2f8b28e0896d0b32a658fb359bf -size 25430 +oid sha256:4bdedf444797479efa73d6ca7d3d6545f4bd01faa80d50980b0ad6ea427b09ee +size 20828 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png index 968d9fdff5..7c2736059e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:110139dd6177e5e1bd126bcd60e4442394867c2e0a3656bee7f15cc5c58c6a6e -size 32584 +oid sha256:51774321b68ee4d1f5a605edf1ce630e0f7651191e93cab6a885fc90f1639061 +size 24493 From 46f78ef70083a0f0eeaad17401c36b09f2d7fc1b Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 19 Oct 2023 17:38:43 +0200 Subject: [PATCH 083/281] Integrate Element Call with widget API (#1581) * Integrate Element Call with widget API. - Add `appconfig` module and extract constants that can be overridden in forks there. - Add an Element Call feature flag, disabled by default. - Refactor the whole `ElementCallActivity`, move most logic out of it. - Integrate with the Rust Widget Driver API (note the Rust SDK version used in this PR lacks some needed changes to make the calls actually work). - Handle calls differently based on `CallType`. - Add UI to create/join a call. --------- Co-authored-by: ElementBot --- appconfig/build.gradle.kts | 24 +++ .../android/appconfig/AuthenticationConfig.kt | 24 +++ .../android/appconfig/ElementCallConfig.kt | 21 ++ .../android/appconfig}/MatrixConfiguration.kt | 2 +- features/call/build.gradle.kts | 26 ++- features/call/src/main/AndroidManifest.xml | 2 +- .../features/call/CallForegroundService.kt | 1 + .../element/android/features/call/CallType.kt | 34 +++ .../features/call/data/WidgetMessage.kt | 43 ++++ .../android/features/call/di/CallBindings.kt | 2 +- .../features/call/ui/CallScreeEvents.kt | 24 +++ .../features/call/ui/CallScreenPresenter.kt | 172 ++++++++++++++++ .../features/call/ui/CallScreenState.kt | 26 +++ .../features/call/{ => ui}/CallScreenView.kt | 141 +++++++------ .../call/{ => ui}/ElementCallActivity.kt | 96 ++++++--- .../call/{ => utils}/CallIntentDataParser.kt | 2 +- .../features/call/utils/CallWidgetProvider.kt | 31 +++ .../call/utils/DefaultCallWidgetProvider.kt | 50 +++++ .../utils/WebViewWidgetMessageInterceptor.kt | 100 +++++++++ .../call/utils/WidgetMessageInterceptor.kt | 24 +++ .../call/utils/WidgetMessageSerializer.kt | 33 +++ .../features/call/MapWebkitPermissionsTest.kt | 1 + .../call/ui/CallScreenPresenterTest.kt | 194 ++++++++++++++++++ .../call/ui/FakeCallScreenNavigator.kt | 26 +++ .../{ => utils}/CallIntentDataParserTest.kt | 2 +- .../utils/DefaultCallWidgetProviderTest.kt | 121 +++++++++++ .../call/utils/FakeCallWidgetProvider.kt | 42 ++++ .../utils/FakeWidgetMessageInterceptor.kt | 33 +++ features/login/impl/build.gradle.kts | 1 + .../AccountProviderProvider.kt | 4 +- .../ChangeAccountProviderPresenter.kt | 4 +- .../SearchAccountProviderStateProvider.kt | 4 +- .../SearchAccountProviderView.kt | 7 +- .../login/impl/util/LoginConstants.kt | 14 +- .../android/features/login/impl/util/Util.kt | 3 +- .../api/src/main/res/values/localazy.xml | 4 + features/messages/impl/build.gradle.kts | 1 + .../messages/impl/MessagesFlowNode.kt | 15 ++ .../features/messages/impl/MessagesNode.kt | 7 + .../messages/impl/MessagesPresenter.kt | 3 + .../features/messages/impl/MessagesState.kt | 1 + .../messages/impl/MessagesStateProvider.kt | 1 + .../features/messages/impl/MessagesView.kt | 16 ++ features/preferences/impl/build.gradle.kts | 1 + .../impl/advanced/AdvancedSettingsEvents.kt | 1 + .../advanced/AdvancedSettingsPresenter.kt | 38 ++++ .../impl/advanced/AdvancedSettingsState.kt | 9 +- .../advanced/AdvancedSettingsStateProvider.kt | 3 + .../impl/advanced/AdvancedSettingsView.kt | 27 ++- .../impl/src/main/res/values/localazy.xml | 3 + .../advanced/AdvancedSettingsPresenterTest.kt | 71 ++++++- .../libraries/architecture/Bindings.kt | 8 +- .../components/dialogs/ListDialog.kt | 6 +- .../components/list/TextFieldListItem.kt | 65 +++++- .../preferences/PreferenceTextField.kt | 141 +++++++++++++ .../theme/components/AlertDialogContent.kt | 2 + .../libraries/featureflag/api/FeatureFlags.kt | 6 + .../impl/StaticFeatureFlagProvider.kt | 1 + libraries/matrix/api/build.gradle.kts | 1 + .../matrix/api/permalink/MatrixToConverter.kt | 2 +- .../matrix/api/permalink/PermalinkBuilder.kt | 2 +- .../libraries/matrix/api/room/MatrixRoom.kt | 24 +++ .../api/widget/CallWidgetSettingsProvider.kt | 26 +++ .../matrix/api/widget/MatrixWidgetDriver.kt | 27 +++ .../matrix/api/widget/MatrixWidgetSettings.kt | 29 +++ .../matrix/impl/room/RustMatrixRoom.kt | 27 +++ .../DefaultCallWidgetSettingsProvider.kt | 46 +++++ .../impl/widget/MatrixWidgetSettings.kt | 50 +++++ .../matrix/impl/widget/RustWidgetDriver.kt | 78 +++++++ .../libraries/matrix/test/FakeMatrixClient.kt | 8 +- .../matrix/test/FakeMatrixClientProvider.kt | 27 +++ .../matrix/test/room/FakeMatrixRoom.kt | 22 ++ .../widget/FakeCallWidgetSettingsProvider.kt | 32 +++ .../matrix/test/widget/FakeWidgetDriver.kt | 52 +++++ .../preferences/api/store/PreferencesStore.kt | 3 + .../impl/store/DefaultPreferencesStore.kt | 18 ++ .../test/InMemoryPreferencesStore.kt | 10 + .../src/main/res/values-cs/translations.xml | 5 +- .../src/main/res/values-ru/translations.xml | 1 - .../src/main/res/values-sk/translations.xml | 1 - .../src/main/res/values/localazy.xml | 42 +++- settings.gradle.kts | 1 + ...lScreenView-D-0_0_null,NEXUS_5,1.0,en].png | 3 + ...lScreenView-N-0_1_null,NEXUS_5,1.0,en].png | 3 + ...lScreenView-D-0_0_null,NEXUS_5,1.0,en].png | 3 - ...lScreenView-N-0_1_null,NEXUS_5,1.0,en].png | 3 - ...sagesView-D-0_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_4,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_5,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_6,NEXUS_5,1.0,en].png | 4 +- ...tingsView-D-1_1_null_3,NEXUS_5,1.0,en].png | 3 + ...tingsView-N-1_2_null_3,NEXUS_5,1.0,en].png | 3 + ...-textfieldvalue_0_null,NEXUS_5,1.0,en].png | 3 + tools/localazy/config.json | 1 + 102 files changed, 2202 insertions(+), 166 deletions(-) create mode 100644 appconfig/build.gradle.kts create mode 100644 appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt create mode 100644 appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt rename {libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/config => appconfig/src/main/kotlin/io/element/android/appconfig}/MatrixConfiguration.kt (93%) create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/CallType.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt rename features/call/src/main/kotlin/io/element/android/features/call/{ => ui}/CallScreenView.kt (52%) rename features/call/src/main/kotlin/io/element/android/features/call/{ => ui}/ElementCallActivity.kt (72%) rename features/call/src/main/kotlin/io/element/android/features/call/{ => utils}/CallIntentDataParser.kt (98%) create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt create mode 100644 features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt rename features/call/src/test/kotlin/io/element/android/features/call/{ => utils}/CallIntentDataParserTest.kt (99%) create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt create mode 100644 features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetSettings.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeWidgetDriver.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_TextfieldListitem-textfieldvalue_0_null,NEXUS_5,1.0,en].png diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts new file mode 100644 index 0000000000..3c03739553 --- /dev/null +++ b/appconfig/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * 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. + */ +plugins { + id("java-library") + alias(libs.plugins.kotlin.jvm) +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt new file mode 100644 index 0000000000..186b84f8f0 --- /dev/null +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/AuthenticationConfig.kt @@ -0,0 +1,24 @@ +/* + * 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.appconfig + +object AuthenticationConfig { + const val MATRIX_ORG_URL = "https://matrix.org" + + const val DEFAULT_HOMESERVER_URL = MATRIX_ORG_URL + const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" +} diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt new file mode 100644 index 0000000000..bbd9f62689 --- /dev/null +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt @@ -0,0 +1,21 @@ +/* + * 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.appconfig + +object ElementCallConfig { + const val DEFAULT_BASE_URL = "https://call.element.io" +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/config/MatrixConfiguration.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt similarity index 93% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/config/MatrixConfiguration.kt rename to appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt index ddce776627..e4d6ee7ca2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/config/MatrixConfiguration.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/MatrixConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.api.config +package io.element.android.appconfig object MatrixConfiguration { const val matrixToPermalinkBaseUrl: String = "https://matrix.to/#/" diff --git a/features/call/build.gradle.kts b/features/call/build.gradle.kts index 69046e33b4..c59f1ea855 100644 --- a/features/call/build.gradle.kts +++ b/features/call/build.gradle.kts @@ -18,20 +18,44 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) alias(libs.plugins.ksp) + id("kotlin-parcelize") + alias(libs.plugins.kotlin.serialization) } android { namespace = "io.element.android.features.call" + + buildFeatures { + buildConfig = true + } +} + +anvil { + generateDaggerFactories.set(true) } dependencies { + implementation(projects.appnav) + implementation(projects.appconfig) + implementation(projects.anvilannotations) implementation(projects.libraries.architecture) + implementation(projects.libraries.core) implementation(projects.libraries.designsystem) + implementation(projects.libraries.matrix.impl) implementation(projects.libraries.network) + implementation(projects.libraries.preferences.api) + implementation(projects.services.toolbox.api) implementation(libs.androidx.webkit) + implementation(libs.serialization.json) ksp(libs.showkase.processor) - testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) testImplementation(libs.test.robolectric) + testImplementation(projects.libraries.featureflag.test) + testImplementation(projects.libraries.preferences.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/features/call/src/main/AndroidManifest.xml b/features/call/src/main/AndroidManifest.xml index 877b7fb0a8..c7db9cc38f 100644 --- a/features/call/src/main/AndroidManifest.xml +++ b/features/call/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ { + + @AssistedFactory + interface Factory { + fun create(callType: CallType, navigator: CallScreenNavigator): CallScreenPresenter + } + + private val isInWidgetMode = callType is CallType.RoomCall + private val userAgent = userAgentProvider.provide() + + @Composable + override fun present(): CallScreenState { + val coroutineScope = rememberCoroutineScope() + val urlState = remember { mutableStateOf>(Async.Uninitialized) } + val callWidgetDriver = remember { mutableStateOf(null) } + val messageInterceptor = remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + loadUrl(callType, urlState, callWidgetDriver) + } + + callWidgetDriver.value?.let { driver -> + LaunchedEffect(Unit) { + driver.incomingMessages + .onEach { + // Relay message to the WebView + messageInterceptor.value?.sendMessage(it) + } + .launchIn(this) + + driver.run() + } + } + + messageInterceptor.value?.let { interceptor -> + LaunchedEffect(Unit) { + interceptor.interceptedMessages + .onEach { + // Relay message to Widget Driver + callWidgetDriver.value?.send(it) + + val parsedMessage = parseMessage(it) + if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget && parsedMessage.action == WidgetMessage.Action.HangUp) { + close(callWidgetDriver.value, navigator) + } + } + .launchIn(this) + } + } + + fun handleEvents(event: CallScreeEvents) { + when (event) { + is CallScreeEvents.Hangup -> { + val widgetId = callWidgetDriver.value?.id + val interceptor = messageInterceptor.value + if (widgetId != null && interceptor != null) { + sendHangupMessage(widgetId, interceptor) + } + coroutineScope.launch { + close(callWidgetDriver.value, navigator) + } + } + is CallScreeEvents.SetupMessageChannels -> { + messageInterceptor.value = event.widgetMessageInterceptor + } + } + } + + return CallScreenState( + urlState = urlState.value, + userAgent = userAgent, + isInWidgetMode = isInWidgetMode, + eventSink = ::handleEvents, + ) + } + + private fun CoroutineScope.loadUrl( + inputs: CallType, + urlState: MutableState>, + callWidgetDriver: MutableState, + ) = launch { + urlState.runCatchingUpdatingState { + when (inputs) { + is CallType.ExternalUrl -> { + inputs.url + } + is CallType.RoomCall -> { + val (driver, url) = callWidgetProvider.getWidget( + sessionId = inputs.sessionId, + roomId = inputs.roomId, + clientId = UUID.randomUUID().toString(), + ).getOrThrow() + callWidgetDriver.value = driver + url + } + } + } + } + + private fun parseMessage(message: String): WidgetMessage? { + return WidgetMessageSerializer.deserialize(message).getOrNull() + } + + private fun sendHangupMessage(widgetId: String, messageInterceptor: WidgetMessageInterceptor) { + val message = WidgetMessage( + direction = WidgetMessage.Direction.ToWidget, + widgetId = widgetId, + requestId = "widgetapi-${clock.epochMillis()}", + action = WidgetMessage.Action.HangUp, + ) + messageInterceptor.sendMessage(WidgetMessageSerializer.serialize(message)) + } + + private fun CoroutineScope.close(widgetDriver: MatrixWidgetDriver?, navigator: CallScreenNavigator) = launch(dispatchers.io) { + navigator.close() + widgetDriver?.close() + } + +} + diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt new file mode 100644 index 0000000000..d9716251fc --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt @@ -0,0 +1,26 @@ +/* + * 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.call.ui + +import io.element.android.libraries.architecture.Async + +data class CallScreenState( + val urlState: Async, + val userAgent: String, + val isInWidgetMode: Boolean, + val eventSink: (CallScreeEvents) -> Unit, +) diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt similarity index 52% rename from features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt rename to features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt index 0f5b90cbc8..9ac06a63b7 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/CallScreenView.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt @@ -14,106 +14,128 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.ui import android.annotation.SuppressLint import android.view.ViewGroup import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebView +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView +import io.element.android.features.call.R +import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor +import io.element.android.libraries.architecture.Async 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.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.designsystem.utils.CommonDrawables -import io.element.android.libraries.theme.ElementTheme typealias RequestPermissionCallback = (Array) -> Unit +interface CallScreenNavigator { + fun close() +} + @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun CallScreenView( - url: String?, - userAgent: String, + state: CallScreenState, requestPermissions: (Array, RequestPermissionCallback) -> Unit, - onClose: () -> Unit, modifier: Modifier = Modifier, ) { - ElementTheme { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.element_call)) }, - navigationIcon = { - BackButton( - resourceId = CommonDrawables.ic_compound_close, - onClick = onClose - ) - } - ) - } - ) { padding -> - CallWebView( - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - .fillMaxSize(), - url = url, - userAgent = userAgent, - onPermissionsRequested = { request -> - val androidPermissions = mapWebkitPermissions(request.resources) - val callback: RequestPermissionCallback = { request.grant(it) } - requestPermissions(androidPermissions.toTypedArray(), callback) + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.element_call)) }, + navigationIcon = { + BackButton( + resourceId = CommonDrawables.ic_compound_close, + onClick = { state.eventSink(CallScreeEvents.Hangup) } + ) } ) } + ) { padding -> + BackHandler { + state.eventSink(CallScreeEvents.Hangup) + } + CallWebView( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .fillMaxSize(), + url = state.urlState, + userAgent = state.userAgent, + onPermissionsRequested = { request -> + val androidPermissions = mapWebkitPermissions(request.resources) + val callback: RequestPermissionCallback = { request.grant(it) } + requestPermissions(androidPermissions.toTypedArray(), callback) + }, + onWebViewCreated = { webView -> + val interceptor = WebViewWidgetMessageInterceptor(webView) + state.eventSink(CallScreeEvents.SetupMessageChannels(interceptor)) + } + ) } } @Composable private fun CallWebView( - url: String?, + url: Async, userAgent: String, onPermissionsRequested: (PermissionRequest) -> Unit, + onWebViewCreated: (WebView) -> Unit, modifier: Modifier = Modifier, ) { - val isInpectionMode = LocalInspectionMode.current - AndroidView( - modifier = modifier, - factory = { context -> - WebView(context).apply { - if (!isInpectionMode) { - setup(userAgent, onPermissionsRequested) - if (url != null) { - loadUrl(url) - } - } - } - }, - update = { webView -> - if (!isInpectionMode && url != null) { - webView.loadUrl(url) - } - }, - onRelease = { webView -> - webView.destroy() + if (LocalInspectionMode.current) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Text("WebView - can't be previewed") } - ) + } else { + AndroidView( + modifier = modifier, + factory = { context -> + WebView(context).apply { + setup(userAgent, onPermissionsRequested) + if (url is Async.Success) { + loadUrl(url.data) + } + + onWebViewCreated(this) + } + }, + update = { webView -> + if (url is Async.Success && webView.url != url.data) { + webView.loadUrl(url.data) + } + }, + onRelease = { webView -> + webView.destroy() + } + ) + } } @SuppressLint("SetJavaScriptEnabled") -private fun WebView.setup(userAgent: String, onPermissionsRequested: (PermissionRequest) -> Unit) { +private fun WebView.setup( + userAgent: String, + onPermissionsRequested: (PermissionRequest) -> Unit, +) { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT @@ -140,12 +162,15 @@ private fun WebView.setup(userAgent: String, onPermissionsRequested: (Permission @PreviewsDayNight @Composable internal fun CallScreenViewPreview() { - ElementTheme { + ElementPreview { CallScreenView( - url = "https://call.element.io/some-actual-call?with=parameters", - userAgent = "", + state = CallScreenState( + urlState = Async.Success("https://call.element.io/some-actual-call?with=parameters"), + isInWidgetMode = false, + userAgent = "", + eventSink = {}, + ), requestPermissions = { _, _ -> }, - onClose = { }, ) } } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt similarity index 72% rename from features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt rename to features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt index 481634a4ca..651a2176f3 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt @@ -14,10 +14,12 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.ui import android.Manifest +import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.res.Configuration import android.media.AudioAttributes import android.media.AudioFocusRequest @@ -26,20 +28,40 @@ import android.os.Build import android.os.Bundle import android.view.WindowManager import android.webkit.PermissionRequest -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.mutableStateOf +import androidx.core.content.IntentCompat +import com.bumble.appyx.core.integrationpoint.NodeComponentActivity +import io.element.android.features.call.CallForegroundService +import io.element.android.features.call.CallType import io.element.android.features.call.di.CallBindings +import io.element.android.features.call.utils.CallIntentDataParser import io.element.android.libraries.architecture.bindings -import io.element.android.libraries.network.useragent.UserAgentProvider +import io.element.android.libraries.theme.ElementTheme import javax.inject.Inject -class ElementCallActivity : ComponentActivity() { +class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator { + companion object { + private const val EXTRA_CALL_WIDGET_SETTINGS = "EXTRA_CALL_WIDGET_SETTINGS" + + fun start( + context: Context, + callInputs: CallType, + ) { + val intent = Intent(context, ElementCallActivity::class.java).apply { + putExtra(EXTRA_CALL_WIDGET_SETTINGS, callInputs) + addFlags(FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + } - @Inject lateinit var userAgentProvider: UserAgentProvider @Inject lateinit var callIntentDataParser: CallIntentDataParser + @Inject lateinit var presenterFactory: CallScreenPresenter.Factory + + private lateinit var presenter: CallScreenPresenter private lateinit var audioManager: AudioManager @@ -51,7 +73,7 @@ class ElementCallActivity : ComponentActivity() { private val requestPermissionsLauncher = registerPermissionResultLauncher() private var isDarkMode = false - private val urlState = mutableStateOf(null) + private val webViewTarget = mutableStateOf(null) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -60,10 +82,7 @@ class ElementCallActivity : ComponentActivity() { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - urlState.value = intent?.dataString?.let(::parseUrl) ?: run { - finish() - return - } + setCallType(intent) if (savedInstanceState == null) { updateUiMode(resources.configuration) @@ -72,18 +91,17 @@ class ElementCallActivity : ComponentActivity() { audioManager = getSystemService(AUDIO_SERVICE) as AudioManager requestAudioFocus() - val userAgent = userAgentProvider.provide() - setContent { - CallScreenView( - url = urlState.value, - userAgent = userAgent, - onClose = this::finish, - requestPermissions = { permissions, callback -> - requestPermissionCallback = callback - requestPermissionsLauncher.launch(permissions) - } - ) + val state = presenter.present() + ElementTheme { + CallScreenView( + state = state, + requestPermissions = { permissions, callback -> + requestPermissionCallback = callback + requestPermissionsLauncher.launch(permissions) + } + ) + } } } @@ -96,15 +114,7 @@ class ElementCallActivity : ComponentActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - val intentUrl = intent?.dataString?.let(::parseUrl) - when { - // New URL, update it and reload the webview - intentUrl != null -> urlState.value = intentUrl - // Re-opened the activity but we have no url to load or a cached one, finish the activity - intent?.dataString == null && urlState.value == null -> finish() - // Coming back from notification, do nothing - else -> return - } + setCallType(intent) } override fun onStart() { @@ -130,6 +140,32 @@ class ElementCallActivity : ComponentActivity() { finishAndRemoveTask() } + override fun close() { + finish() + } + + private fun setCallType(intent: Intent?) { + val inputs = intent?.let { + IntentCompat.getParcelableExtra(it, EXTRA_CALL_WIDGET_SETTINGS, CallType::class.java) + } + val intentUrl = intent?.dataString?.let(::parseUrl) + when { + // Re-opened the activity but we have no url to load or a cached one, finish the activity + intent?.dataString == null && inputs == null && webViewTarget.value == null -> finish() + inputs != null -> { + webViewTarget.value = inputs + presenter = presenterFactory.create(inputs, this) + } + intentUrl != null -> { + val fallbackInputs = CallType.ExternalUrl(intentUrl) + webViewTarget.value = fallbackInputs + presenter = presenterFactory.create(fallbackInputs, this) + } + // Coming back from notification, do nothing + else -> return + } + } + private fun parseUrl(url: String?): String? = callIntentDataParser.parse(url) private fun registerPermissionResultLauncher(): ActivityResultLauncher> { diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt similarity index 98% rename from features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt rename to features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt index b903b437d8..0814216745 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/CallIntentDataParser.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.utils import android.net.Uri import javax.inject.Inject diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt new file mode 100644 index 0000000000..b65298854d --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/CallWidgetProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.call.utils + +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.widget.MatrixWidgetDriver + +interface CallWidgetProvider { + suspend fun getWidget( + sessionId: SessionId, + roomId: RoomId, + clientId: String, + languageTag: String? = null, + theme: String? = null, + ): Result> +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt new file mode 100644 index 0000000000..f3cb9cbcd5 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt @@ -0,0 +1,50 @@ +/* + * 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.call.utils + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.ElementCallConfig +import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.MatrixClientProvider +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.widget.CallWidgetSettingsProvider +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultCallWidgetProvider @Inject constructor( + private val matrixClientsProvider: MatrixClientProvider, + private val preferencesStore: PreferencesStore, + private val callWidgetSettingsProvider: CallWidgetSettingsProvider, +) : CallWidgetProvider { + override suspend fun getWidget( + sessionId: SessionId, + roomId: RoomId, + clientId: String, + languageTag: String?, + theme: String?, + ): Result> = runCatching { + val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found") + val baseUrl = preferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL + val widgetSettings = callWidgetSettingsProvider.provide(baseUrl) + val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow() + room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt new file mode 100644 index 0000000000..bdb6ee48f5 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt @@ -0,0 +1,100 @@ +/* + * 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.call.utils + +import android.graphics.Bitmap +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature +import io.element.android.features.call.BuildConfig +import kotlinx.coroutines.flow.MutableSharedFlow + +class WebViewWidgetMessageInterceptor( + private val webView: WebView, +) : WidgetMessageInterceptor { + + companion object { + // We call both the WebMessageListener and the JavascriptInterface objects in JS with this + // 'listenerName' so they can both receive the data from the WebView when + // `${LISTENER_NAME}.postMessage(...)` is called + const val LISTENER_NAME = "elementX" + } + + override val interceptedMessages = MutableSharedFlow(replay = 1, extraBufferCapacity = 2) + + init { + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + // We inject this JS code when the page starts loading to attach a message listener to the window. + // This listener will receive both messages: + // - EC widget API -> Element X (message.data.api == "fromWidget") + // - Element X -> EC widget API (message.data.api == "toWidget"), we should ignore these + view?.evaluateJavascript( + """ + window.addEventListener('message', function(event) { + let message = {data: event.data, origin: event.origin} + if (message.data.response && message.data.api == "toWidget" + || !message.data.response && message.data.api == "fromWidget") { + let json = JSON.stringify(event.data) + ${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } } + ${LISTENER_NAME}.postMessage(json); + } else { + ${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG } } + } + }); + """.trimIndent(), + null + ) + } + } + + // Create a WebMessageListener, which will receive messages from the WebView and reply to them + val webMessageListener = WebViewCompat.WebMessageListener { _, message, _, _, _ -> + onMessageReceived(message.data) + } + + // Use WebMessageListener if supported, otherwise use JavascriptInterface + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener( + webView, + LISTENER_NAME, + setOf("*"), + webMessageListener + ) + } else { + webView.addJavascriptInterface(object { + @JavascriptInterface + fun postMessage(json: String?) { + onMessageReceived(json) + } + }, LISTENER_NAME) + } + } + + override fun sendMessage(message: String) { + webView.evaluateJavascript("postMessage($message, '*')", null) + } + + private fun onMessageReceived(json: String?) { + // Here is where we would handle the messages from the WebView, passing them to the Rust SDK + json?.let { interceptedMessages.tryEmit(it) } + } +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt new file mode 100644 index 0000000000..fa5c3bea67 --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageInterceptor.kt @@ -0,0 +1,24 @@ +/* + * 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.call.utils + +import kotlinx.coroutines.flow.Flow + +interface WidgetMessageInterceptor { + val interceptedMessages: Flow + fun sendMessage(message: String) +} diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt new file mode 100644 index 0000000000..5ed9db028c --- /dev/null +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/WidgetMessageSerializer.kt @@ -0,0 +1,33 @@ +/* + * 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.call.utils + +import io.element.android.features.call.data.WidgetMessage +import kotlinx.serialization.json.Json + +object WidgetMessageSerializer { + + private val coder = Json { ignoreUnknownKeys = true } + + fun deserialize(message: String): Result { + return runCatching { coder.decodeFromString(WidgetMessage.serializer(), message) } + } + + fun serialize(message: WidgetMessage): String { + return coder.encodeToString(WidgetMessage.serializer(), message) + } +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt index f82e31c068..55b5f16771 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/MapWebkitPermissionsTest.kt @@ -19,6 +19,7 @@ package io.element.android.features.call import android.Manifest import android.webkit.PermissionRequest import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.ui.mapWebkitPermissions import org.junit.Test class MapWebkitPermissionsTest { diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt new file mode 100644 index 0000000000..c318b1dfaa --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -0,0 +1,194 @@ +/* + * 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.call.ui + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.call.CallType +import io.element.android.features.call.utils.FakeCallWidgetProvider +import io.element.android.features.call.utils.FakeWidgetMessageInterceptor +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver +import io.element.android.libraries.network.useragent.UserAgentProvider +import io.element.android.services.toolbox.api.systemclock.SystemClock +import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class CallScreenPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - with CallType ExternalUrl just loads the URL`() = runTest { + val presenter = createCallScreenPresenter(CallType.ExternalUrl("https://call.element.io")) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Wait until the URL is loaded + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.urlState).isEqualTo(Async.Success("https://call.element.io")) + assertThat(initialState.isInWidgetMode).isFalse() + } + } + + @Test + fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest { + val widgetDriver = FakeWidgetDriver() + val widgetProvider = FakeCallWidgetProvider(widgetDriver) + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + widgetProvider = widgetProvider, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Wait until the URL is loaded + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.urlState).isInstanceOf(Async.Success::class.java) + assertThat(initialState.isInWidgetMode).isTrue() + assertThat(widgetProvider.getWidgetCalled).isTrue() + assertThat(widgetDriver.runCalledCount).isEqualTo(1) + } + } + + @Test + fun `present - set message interceptor, send and receive messages`() = runTest { + val widgetDriver = FakeWidgetDriver() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + ) + val messageInterceptor = FakeWidgetMessageInterceptor() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + + // And incoming message from the Widget Driver is passed to the WebView + widgetDriver.givenIncomingMessage("A message") + assertThat(messageInterceptor.sentMessages).containsExactly("A message") + + // And incoming message from the WebView is passed to the Widget Driver + messageInterceptor.givenInterceptedMessage("A reply") + assertThat(widgetDriver.sentMessages).containsExactly("A reply") + + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeWidgetDriver() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + ) + val messageInterceptor = FakeWidgetMessageInterceptor() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + + initialState.eventSink(CallScreeEvents.Hangup) + + // Let background coroutines run + runCurrent() + + assertThat(navigator.closeCalled).isTrue() + assertThat(widgetDriver.closeCalledCount).isEqualTo(1) + + cancelAndIgnoreRemainingEvents() + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeWidgetDriver() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + ) + val messageInterceptor = FakeWidgetMessageInterceptor() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + + messageInterceptor.givenInterceptedMessage("""{"action":"im.vector.hangup","api":"fromWidget","widgetId":"1","requestId":"1"}""") + + // Let background coroutines run + runCurrent() + + assertThat(navigator.closeCalled).isTrue() + assertThat(widgetDriver.closeCalledCount).isEqualTo(1) + + cancelAndIgnoreRemainingEvents() + } + } + + private fun TestScope.createCallScreenPresenter( + callType: CallType, + navigator: CallScreenNavigator = FakeCallScreenNavigator(), + widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), + widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + ): CallScreenPresenter { + val userAgentProvider = object : UserAgentProvider { + override fun provide(): String { + return "Test" + } + } + val clock = SystemClock { 0 } + return CallScreenPresenter( + callType, + navigator, + widgetProvider, + userAgentProvider, + clock, + dispatchers, + ) + } +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt b/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt new file mode 100644 index 0000000000..498503cb15 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/ui/FakeCallScreenNavigator.kt @@ -0,0 +1,26 @@ +/* + * 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.call.ui + +class FakeCallScreenNavigator : CallScreenNavigator { + var closeCalled = false + private set + + override fun close() { + closeCalled = true + } + } diff --git a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt similarity index 99% rename from features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt rename to features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt index ae82767f45..eb8e756182 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTest.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.call +package io.element.android.features.call.utils import com.google.common.truth.Truth.assertThat import org.junit.Test diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt new file mode 100644 index 0000000000..f7f17d794d --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt @@ -0,0 +1,121 @@ +/* + * 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.call.utils + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider +import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultCallWidgetProviderTest { + + @Test + fun `getWidget - fails if the session does not exist`() = runTest { + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.failure(Exception("Session not found")) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() + } + + @Test + fun `getWidget - fails if the room does not exist`() = runTest { + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, null) + } + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() + } + + @Test + fun `getWidget - fails if it can't generate the URL for the widget`() = runTest { + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.failure(Exception("Can't generate URL for widget"))) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() + } + + @Test + fun `getWidget - fails if it can't get the widget driver`() = runTest { + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.success("url")) + givenGetWidgetDriverResult(Result.failure(Exception("Can't get a widget driver"))) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue() + } + + @Test + fun `getWidget - returns a widget driver when all steps are successful`() = runTest { + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.success("url")) + givenGetWidgetDriverResult(Result.success(FakeWidgetDriver())) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }) + assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull() + } + + @Test + fun `getWidget - will use a custom base url if it exists`() = runTest { + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.success("url")) + givenGetWidgetDriverResult(Result.success(FakeWidgetDriver())) + } + val client = FakeMatrixClient().apply { + givenGetRoomResult(A_ROOM_ID, room) + } + val preferencesStore = InMemoryPreferencesStore().apply { + setCustomElementCallBaseUrl("https://custom.element.io") + } + val settingsProvider = FakeCallWidgetSettingsProvider() + val provider = createProvider( + matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }, + callWidgetSettingsProvider = settingsProvider, + preferencesStore = preferencesStore, + ) + provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme") + + assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io") + } + + private fun createProvider( + matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), + preferencesStore: PreferencesStore = InMemoryPreferencesStore(), + callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider() + ) = DefaultCallWidgetProvider( + matrixClientProvider, + preferencesStore, + callWidgetSettingsProvider, + ) +} diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt new file mode 100644 index 0000000000..69ae340648 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.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.call.utils + +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.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver + +class FakeCallWidgetProvider( + private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), + private val url: String = "https://call.element.io", + ) : CallWidgetProvider { + + var getWidgetCalled = false + private set + + override suspend fun getWidget( + sessionId: SessionId, + roomId: RoomId, + clientId: String, + languageTag: String?, + theme: String? + ): Result> { + getWidgetCalled = true + return Result.success(widgetDriver to url) + } + } diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt new file mode 100644 index 0000000000..6e36dfff81 --- /dev/null +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/FakeWidgetMessageInterceptor.kt @@ -0,0 +1,33 @@ +/* + * 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.call.utils + +import kotlinx.coroutines.flow.MutableSharedFlow + +class FakeWidgetMessageInterceptor : WidgetMessageInterceptor { + val sentMessages = mutableListOf() + + override val interceptedMessages = MutableSharedFlow(extraBufferCapacity = 1) + + override fun sendMessage(message: String) { + sentMessages += message + } + + fun givenInterceptedMessage(message: String) { + interceptedMessages.tryEmit(message) + } + } diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index ae13197f05..6f4e959499 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -38,6 +38,7 @@ anvil { dependencies { implementation(projects.anvilannotations) + implementation(projects.appconfig) anvil(projects.anvilcodegen) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt index 35fd7246f2..0d3b9c5dc3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.login.impl.accountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.util.LoginConstants +import io.element.android.appconfig.AuthenticationConfig open class AccountProviderProvider : PreviewParameterProvider { override val values: Sequence @@ -32,7 +32,7 @@ open class AccountProviderProvider : PreviewParameterProvider { } fun anAccountProvider() = AccountProvider( - url = LoginConstants.MATRIX_ORG_URL, + url = AuthenticationConfig.MATRIX_ORG_URL, subtitle = "Matrix.org is an open network for secure, decentralized communication.", isPublic = true, isMatrixOrg = true, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt index 786d8aaeae..96fc115cfa 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -17,9 +17,9 @@ package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.runtime.Composable +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.changeserver.ChangeServerPresenter -import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -34,7 +34,7 @@ class ChangeAccountProviderPresenter @Inject constructor( // Just matrix.org by default for now accountProviders = listOf( AccountProvider( - url = LoginConstants.MATRIX_ORG_URL, + url = AuthenticationConfig.MATRIX_ORG_URL, subtitle = null, isPublic = true, isMatrixOrg = true, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt index 8dce1bd78e..50b24b3964 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt @@ -17,9 +17,9 @@ package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverData -import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async open class SearchAccountProviderStateProvider : PreviewParameterProvider { @@ -50,7 +50,7 @@ fun aHomeserverDataList(): List { } fun aHomeserverData( - homeserverUrl: String = LoginConstants.MATRIX_ORG_URL, + homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL, isWellknownValid: Boolean = true, supportSlidingSync: Boolean = true, ): HomeserverData { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index 29781acff1..47dfe248b7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt @@ -14,12 +14,11 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@file:OptIn(ExperimentalMaterial3Api::class) package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -48,13 +47,13 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.R import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderView import io.element.android.features.login.impl.changeserver.ChangeServerEvents import io.element.android.features.login.impl.changeserver.ChangeServerView import io.element.android.features.login.impl.resolver.HomeserverData -import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton @@ -196,7 +195,7 @@ fun SearchAccountProviderView( @Composable private fun HomeserverData.toAccountProvider(): AccountProvider { - val isMatrixOrg = homeserverUrl == LoginConstants.MATRIX_ORG_URL + val isMatrixOrg = homeserverUrl == AuthenticationConfig.MATRIX_ORG_URL return AccountProvider( url = homeserverUrl, subtitle = if (isMatrixOrg) stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle) else null, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt index 98fd62d7b0..91c19e4052 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt @@ -16,18 +16,12 @@ package io.element.android.features.login.impl.util +import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.login.impl.accountprovider.AccountProvider -object LoginConstants { - const val MATRIX_ORG_URL = "https://matrix.org" - - const val DEFAULT_HOMESERVER_URL = "https://matrix.org" - const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" -} - val defaultAccountProvider = AccountProvider( - url = LoginConstants.DEFAULT_HOMESERVER_URL, + url = AuthenticationConfig.DEFAULT_HOMESERVER_URL, subtitle = null, - isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, - isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, + isPublic = AuthenticationConfig.DEFAULT_HOMESERVER_URL == AuthenticationConfig.MATRIX_ORG_URL, + isMatrixOrg = AuthenticationConfig.DEFAULT_HOMESERVER_URL == AuthenticationConfig.MATRIX_ORG_URL, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt index 261b02c1b8..6726105bce 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt @@ -19,9 +19,10 @@ package io.element.android.features.login.impl.util import android.content.Context import android.content.Intent import android.net.Uri +import io.element.android.appconfig.AuthenticationConfig import io.element.android.libraries.core.data.tryOrNull fun openLearnMorePage(context: Context) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL)) + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(AuthenticationConfig.SLIDING_SYNC_READ_MORE_URL)) tryOrNull { context.startActivity(intent) } } diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index 9ea4bb77fd..5a5c9c64ca 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -1,8 +1,12 @@ + "Please wait for this to complete before signing out." + "Your keys are still being backed up" "Are you sure you want to sign out?" "Sign out" "Signing out…" + "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." + "Have you saved your recovery key?" "Sign out" "Sign out" diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index e886a3aeaa..956b80949a 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) api(projects.features.messages.api) + implementation(projects.features.call) implementation(projects.features.location.api) implementation(projects.features.poll.api) implementation(projects.libraries.androidutils) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 21e384906e..128c531374 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl +import android.content.Context import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -29,6 +30,8 @@ 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.call.CallType +import io.element.android.features.call.ui.ElementCallActivity import io.element.android.features.location.api.Location import io.element.android.features.location.api.SendLocationEntryPoint import io.element.android.features.location.api.ShowLocationEntryPoint @@ -50,7 +53,9 @@ import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope +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.UserId @@ -63,6 +68,8 @@ import kotlinx.parcelize.Parcelize class MessagesFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + @ApplicationContext private val context: Context, + private val matrixClient: MatrixClient, private val sendLocationEntryPoint: SendLocationEntryPoint, private val showLocationEntryPoint: ShowLocationEntryPoint, private val createPollEntryPoint: CreatePollEntryPoint, @@ -149,6 +156,14 @@ class MessagesFlowNode @AssistedInject constructor( override fun onCreatePollClicked() { backstack.push(NavTarget.CreatePoll) } + + override fun onJoinCallClicked(roomId: RoomId) { + val inputs = CallType.RoomCall( + sessionId = matrixClient.sessionId, + roomId = roomId, + ) + ElementCallActivity.start(context, inputs) + } } createNode(buildContext, listOf(callback)) } 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 dbf7e2fbb2..50b59afbcc 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 @@ -33,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories 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.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo @@ -63,6 +64,7 @@ class MessagesNode @AssistedInject constructor( fun onReportMessage(eventId: EventId, senderId: UserId) fun onSendLocationClicked() fun onCreatePollClicked() + fun onJoinCallClicked(roomId: RoomId) } init { @@ -108,6 +110,10 @@ class MessagesNode @AssistedInject constructor( callback?.onCreatePollClicked() } + private fun onJoinCallClicked() { + callback?.onJoinCallClicked(room.roomId) + } + @Composable override fun View(modifier: Modifier) { CompositionLocalProvider( @@ -123,6 +129,7 @@ class MessagesNode @AssistedInject constructor( onUserDataClicked = this::onUserDataClicked, onSendLocationClicked = this::onSendLocationClicked, onCreatePollClicked = this::onCreatePollClicked, + onJoinCallClicked = this::onJoinCallClicked, modifier = modifier, ) } 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 8645553f0b..6125e920b8 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 @@ -152,8 +152,10 @@ class MessagesPresenter @AssistedInject constructor( val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) var enableVoiceMessages by remember { mutableStateOf(false) } + var enableInRoomCalls by remember { mutableStateOf(false) } LaunchedEffect(featureFlagsService) { enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) + enableInRoomCalls = featureFlagsService.isFeatureEnabled(FeatureFlags.InRoomCalls) } fun handleEvents(event: MessagesEvents) { @@ -200,6 +202,7 @@ class MessagesPresenter @AssistedInject constructor( inviteProgress = inviteProgress.value, enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, + enableInRoomCalls = enableInRoomCalls, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 0a121b50a3..3a0585f390 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -49,5 +49,6 @@ data class MessagesState( val showReinvitePrompt: Boolean, val enableTextFormatting: Boolean, val enableVoiceMessages: Boolean, + val enableInRoomCalls: Boolean, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 3b0b87ea39..4222a0889d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -85,5 +85,6 @@ fun aMessagesState() = MessagesState( showReinvitePrompt = false, enableTextFormatting = true, enableVoiceMessages = true, + enableInRoomCalls = true, eventSink = {} ) 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 b79e84a2e0..5a7168e7ce 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 @@ -76,9 +76,12 @@ import io.element.android.libraries.designsystem.components.dialogs.Confirmation import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle +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.TopAppBar +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState @@ -99,6 +102,7 @@ fun MessagesView( onPreviewAttachments: (ImmutableList) -> Unit, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, + onJoinCallClicked: () -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") @@ -160,8 +164,10 @@ fun MessagesView( MessagesViewTopBar( roomName = state.roomName.dataOrNull(), roomAvatar = state.roomAvatar.dataOrNull(), + inRoomCallsEnabled = state.enableInRoomCalls, onBackPressed = onBackPressed, onRoomDetailsClicked = onRoomDetailsClicked, + onJoinCallClicked = onJoinCallClicked, ) } }, @@ -349,8 +355,10 @@ private fun MessagesViewContent( private fun MessagesViewTopBar( roomName: String?, roomAvatar: AvatarData?, + inRoomCallsEnabled: Boolean, modifier: Modifier = Modifier, onRoomDetailsClicked: () -> Unit = {}, + onJoinCallClicked: () -> Unit = {}, onBackPressed: () -> Unit = {}, ) { TopAppBar( @@ -373,6 +381,13 @@ private fun MessagesViewTopBar( ) } }, + actions = { + if (inRoomCallsEnabled) { + IconButton(onClick = onJoinCallClicked) { + Icon(CommonDrawables.ic_compound_video_call, contentDescription = null) // TODO add proper content description once we have the state + } + } + }, windowInsets = WindowInsets(0.dp) ) } @@ -432,5 +447,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onUserDataClicked = {}, onSendLocationClicked = {}, onCreatePollClicked = {}, + onJoinCallClicked = {}, ) } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index a227d24b8b..4fcc69ff6b 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) implementation(projects.libraries.androidutils) + implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt index 37641d684c..fea42baf5f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt @@ -19,4 +19,5 @@ package io.element.android.features.preferences.impl.advanced sealed interface AdvancedSettingsEvents { data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents + data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AdvancedSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index 5738fe43c8..6359b34d0f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -17,16 +17,25 @@ package io.element.android.features.preferences.impl.advanced import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect 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.setValue +import io.element.android.appconfig.ElementCallConfig import io.element.android.features.preferences.api.store.PreferencesStore 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.coroutines.launch +import java.net.URL import javax.inject.Inject class AdvancedSettingsPresenter @Inject constructor( private val preferencesStore: PreferencesStore, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable @@ -38,6 +47,14 @@ class AdvancedSettingsPresenter @Inject constructor( val isDeveloperModeEnabled by preferencesStore .isDeveloperModeEnabledFlow() .collectAsState(initial = false) + val customElementCallBaseUrl by preferencesStore + .getCustomElementCallBaseUrlFlow() + .collectAsState(initial = null) + + var canDisplayElementCallSettings by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + canDisplayElementCallSettings = featureFlagService.isFeatureEnabled(FeatureFlags.InRoomCalls) + } fun handleEvents(event: AdvancedSettingsEvents) { when (event) { @@ -47,13 +64,34 @@ class AdvancedSettingsPresenter @Inject constructor( is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch { preferencesStore.setDeveloperModeEnabled(event.enabled) } + is AdvancedSettingsEvents.SetCustomElementCallBaseUrl -> localCoroutineScope.launch { + // If the URL is either empty or the default one, we want to save 'null' to remove the custom URL + val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL } + preferencesStore.setCustomElementCallBaseUrl(urlToSave) + } } } return AdvancedSettingsState( isRichTextEditorEnabled = isRichTextEditorEnabled, isDeveloperModeEnabled = isDeveloperModeEnabled, + customElementCallBaseUrlState = if (canDisplayElementCallSettings) { + CustomElementCallBaseUrlState( + baseUrl = customElementCallBaseUrl, + defaultUrl = ElementCallConfig.DEFAULT_BASE_URL, + validator = ::customElementCallUrlValidator, + ) + } else null, eventSink = ::handleEvents ) } + + private fun customElementCallUrlValidator(url: String?): Boolean { + return runCatching { + if (url.isNullOrEmpty()) return@runCatching + val parsedUrl = URL(url) + if (parsedUrl.protocol !in listOf("http", "https")) error("Incorrect protocol") + if (parsedUrl.host.isNullOrBlank()) error("Missing host") + }.isSuccess + } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index 19625b9ebc..cd56078b27 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -16,8 +16,15 @@ package io.element.android.features.preferences.impl.advanced -data class AdvancedSettingsState constructor( +data class AdvancedSettingsState( val isRichTextEditorEnabled: Boolean, val isDeveloperModeEnabled: Boolean, + val customElementCallBaseUrlState: CustomElementCallBaseUrlState?, val eventSink: (AdvancedSettingsEvents) -> Unit ) + +data class CustomElementCallBaseUrlState( + val baseUrl: String?, + val defaultUrl: String, + val validator: (String?) -> Boolean, +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt index 5ab50c8a16..d3a2dee3f4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -24,14 +24,17 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider Unit, modifier: Modifier = Modifier, ) { + fun isUsingDefaultUrl(value: String?): Boolean { + val defaultUrl = state.customElementCallBaseUrlState?.defaultUrl ?: return false + return value.isNullOrEmpty() || value == defaultUrl + } + PreferencePage( modifier = modifier, onBackPressed = onBackPressed, @@ -50,6 +58,23 @@ fun AdvancedSettingsView( isChecked = state.isDeveloperModeEnabled, onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) }, ) + state.customElementCallBaseUrlState?.let { callUrlState -> + val supportingText = if (isUsingDefaultUrl(callUrlState.baseUrl)) { + stringResource(R.string.screen_advanced_settings_element_call_base_url_description) + } else { + callUrlState.baseUrl + } + PreferenceTextField( + headline = stringResource(R.string.screen_advanced_settings_element_call_base_url), + value = callUrlState.baseUrl ?: callUrlState.defaultUrl, + supportingText = supportingText, + validation = callUrlState.validator, + onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error), + displayValue = { value -> !isUsingDefaultUrl(value) }, + keyboardOptions = KeyboardOptions.Default.copy(autoCorrect = false, keyboardType = KeyboardType.Uri), + onChange = { state.eventSink(AdvancedSettingsEvents.SetCustomElementCallBaseUrl(it)) } + ) + } } } diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index b94db7a565..1ca7071436 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -1,5 +1,8 @@ + "Custom Element Call base URL" + "Set a custom base URL for Element Call." + "Invalid URL, please make sure you include the protocol (http/https) and the correct address." "Developer mode" "Enable to have access to features and functionality for developers." "Disable the rich text editor to type Markdown manually." diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index 76808ee5f9..11c79657ce 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -20,6 +20,8 @@ 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.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest @@ -34,7 +36,8 @@ class AdvancedSettingsPresenterTest { @Test fun `present - initial state`() = runTest { val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val featureFlagService = FakeFeatureFlagService() + val presenter = AdvancedSettingsPresenter(store, featureFlagService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -47,7 +50,8 @@ class AdvancedSettingsPresenterTest { @Test fun `present - developer mode on off`() = runTest { val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val featureFlagService = FakeFeatureFlagService() + val presenter = AdvancedSettingsPresenter(store, featureFlagService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -63,7 +67,8 @@ class AdvancedSettingsPresenterTest { @Test fun `present - rich text editor on off`() = runTest { val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val featureFlagService = FakeFeatureFlagService() + val presenter = AdvancedSettingsPresenter(store, featureFlagService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -75,4 +80,64 @@ class AdvancedSettingsPresenterTest { assertThat(awaitItem().isRichTextEditorEnabled).isFalse() } } + + @Test + fun `present - custom element call url state is null if the feature flag is disabled`() = runTest { + val store = InMemoryPreferencesStore() + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.InRoomCalls, false) + } + val presenter = AdvancedSettingsPresenter(store, featureFlagService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.customElementCallBaseUrlState).isNull() + } + } + + @Test + fun `present - custom element call base url`() = runTest { + val store = InMemoryPreferencesStore() + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.InRoomCalls, true) + } + val presenter = AdvancedSettingsPresenter(store, featureFlagService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Initial state has a default `false` feature flag value, so the state will still be null + skipItems(1) + + val initialState = awaitItem() + assertThat(initialState.customElementCallBaseUrlState).isNotNull() + assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull() + + initialState.eventSink(AdvancedSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.dev")) + val updatedItem = awaitItem() + assertThat(updatedItem.customElementCallBaseUrlState?.baseUrl).isEqualTo("https://call.element.dev") + } + } + + @Test + fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest { + val store = InMemoryPreferencesStore() + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.InRoomCalls, true) + } + val presenter = AdvancedSettingsPresenter(store, featureFlagService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + // Initial state has a default `false` feature flag value, so the state will still be null + skipItems(1) + + val urlValidator = awaitItem().customElementCallBaseUrlState!!.validator + assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one + assertThat(urlValidator("test")).isFalse() + assertThat(urlValidator("http://")).isFalse() + assertThat(urlValidator("geo://test")).isFalse() + assertThat(urlValidator("https://call.element.io")).isTrue() + } + } } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt index e4a6d7ae7d..f8b6cee0b3 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Bindings.kt @@ -21,6 +21,7 @@ import android.content.ContextWrapper import com.bumble.appyx.core.node.Node import io.element.android.libraries.di.DaggerComponentOwner +inline fun Node.optionalBindings() = optionalBindings(T::class.java) inline fun Node.bindings() = bindings(T::class.java) inline fun Context.bindings() = bindings(T::class.java) @@ -36,7 +37,7 @@ fun Context.bindings(klass: Class): T { ?: error("Unable to find bindings for ${klass.name}") } -fun Node.bindings(klass: Class): T { +fun Node.optionalBindings(klass: Class): T? { // search dagger components in node hierarchy return generateSequence(this, Node::parent) .filterIsInstance() @@ -44,5 +45,8 @@ fun Node.bindings(klass: Class): T { .flatMap { if (it is Collection<*>) it else listOf(it) } .filterIsInstance(klass) .firstOrNull() - ?: error("Unable to find bindings for ${klass.name}") +} + +fun Node.bindings(klass: Class): T { + return optionalBindings(klass) ?: error("Unable to find bindings for ${klass.name}") } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt index e82ebfe532..42cd70cbbd 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ListDialog.kt @@ -27,9 +27,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.airbnb.android.showkase.annotation.ShowkaseComposable import io.element.android.libraries.designsystem.components.list.TextFieldListItem -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.DialogPreview import io.element.android.libraries.designsystem.theme.components.ListSupportingText import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent @@ -45,6 +45,7 @@ fun ListDialog( subtitle: String? = null, cancelText: String = stringResource(CommonStrings.action_cancel), submitText: String = stringResource(CommonStrings.action_ok), + enabled: Boolean = true, listItems: LazyListScope.() -> Unit, ) { val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let { @@ -66,6 +67,7 @@ fun ListDialog( submitText = submitText, onDismissRequest = onDismissRequest, onSubmitClicked = onSubmit, + enabled = enabled, listItems = listItems, ) } @@ -80,6 +82,7 @@ private fun ListDialogContent( submitText: String, modifier: Modifier = Modifier, title: String? = null, + enabled: Boolean = true, subtitle: @Composable (() -> Unit)? = null, ) { SimpleAlertDialogContent( @@ -90,6 +93,7 @@ private fun ListDialogContent( submitText = submitText, onCancelClicked = onDismissRequest, onSubmitClicked = onSubmitClicked, + enabled = enabled, applyPaddingToContents = false, ) { LazyColumn( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt index 525d5e76b1..93268e25d7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/list/TextFieldListItem.kt @@ -16,10 +16,13 @@ package io.element.android.libraries.designsystem.components.list +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -29,24 +32,68 @@ import io.element.android.libraries.theme.ElementTheme @Composable fun TextFieldListItem( - placeholder: String, + placeholder: String?, text: String, onTextChanged: (String) -> Unit, modifier: Modifier = Modifier, + error: String? = null, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, ) { val textFieldStyle = ElementTheme.materialTypography.bodyLarge OutlinedTextField( value = text, - onValueChange = onTextChanged, - placeholder = { Text(placeholder) }, + onValueChange = { onTextChanged(it) }, + placeholder = placeholder?.let { @Composable { Text(it) } }, colors = OutlinedTextFieldDefaults.colors( disabledBorderColor = Color.Transparent, errorBorderColor = Color.Transparent, focusedBorderColor = Color.Transparent, unfocusedBorderColor = Color.Transparent, ), + isError = error != null, + supportingText = error?.let { @Composable { Text(it) } }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, textStyle = textFieldStyle, + maxLines = maxLines, + singleLine = maxLines == 1, + modifier = modifier, + ) +} + +@Composable +fun TextFieldListItem( + placeholder: String?, + text: TextFieldValue, + onTextChanged: (TextFieldValue) -> Unit, + modifier: Modifier = Modifier, + error: String? = null, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + val textFieldStyle = ElementTheme.materialTypography.bodyLarge + + OutlinedTextField( + value = text, + onValueChange = { onTextChanged(it) }, + placeholder = placeholder?.let { @Composable { Text(it) } }, + colors = OutlinedTextFieldDefaults.colors( + disabledBorderColor = Color.Transparent, + errorBorderColor = Color.Transparent, + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + ), + isError = error != null, + supportingText = error?.let { @Composable { Text(it) } }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + textStyle = textFieldStyle, + maxLines = maxLines, + singleLine = maxLines == 1, modifier = modifier, ) } @@ -74,3 +121,15 @@ internal fun TextFieldListItemPreview() { ) } } + +@Preview("Text field List item - textfieldvalue", group = PreviewGroup.ListItems) +@Composable +internal fun TextFieldListItemTextFieldValuePreview() { + ElementThemedPreview { + TextFieldListItem( + placeholder = "Placeholder", + text = TextFieldValue("Text field value"), + onTextChanged = {}, + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt new file mode 100644 index 0000000000..648ea97434 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceTextField.kt @@ -0,0 +1,141 @@ +/* + * 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.components.preferences + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import io.element.android.libraries.designsystem.components.dialogs.ListDialog +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.components.list.TextFieldListItem +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun PreferenceTextField( + headline: String, + onChange: (String?) -> Unit, + modifier: Modifier = Modifier, + placeholder: String? = null, + value: String? = null, + supportingText: String? = null, + displayValue: (String?) -> Boolean = { !it.isNullOrBlank() }, + trailingContent: ListItemContent? = null, + validation: (String?) -> Boolean = { true }, + onValidationErrorMessage: String? = null, + enabled: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + style: ListItemStyle = ListItemStyle.Default, +) { + var displayTextFieldDialog by rememberSaveable { mutableStateOf(false) } + val valueToDisplay = if (displayValue(value)) { value } else supportingText + + ListItem( + modifier = modifier, + headlineContent = { Text(headline) }, + supportingContent = valueToDisplay?.let { @Composable { Text(it) } }, + trailingContent = trailingContent, + style = style, + enabled = enabled, + onClick = { displayTextFieldDialog = true } + ) + + if (displayTextFieldDialog) { + TextFieldDialog( + title = headline, + onSubmit = { + onChange(it.takeIf { it.isNotBlank() }) + displayTextFieldDialog = false + }, + onDismissRequest = { displayTextFieldDialog = false }, + placeholder = placeholder.orEmpty(), + value = value.orEmpty(), + validation = validation, + onValidationErrorMessage = onValidationErrorMessage, + keyboardOptions = keyboardOptions, + ) + } +} + +@Composable +private fun TextFieldDialog( + title: String, + onSubmit: (String) -> Unit, + onDismissRequest: () -> Unit, + value: String?, + placeholder: String?, + modifier: Modifier = Modifier, + validation: (String?) -> Boolean = { true }, + onValidationErrorMessage: String? = null, + autoSelectOnDisplay: Boolean = true, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, +) { + val focusRequester = remember { FocusRequester() } + + var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(value.orEmpty(), selection = TextRange(value.orEmpty().length))) + } + var error by rememberSaveable { mutableStateOf(null) } + val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } } + ListDialog( + title = title, + onSubmit = { onSubmit(textFieldContents.text) }, + onDismissRequest = onDismissRequest, + enabled = canSubmit, + modifier = modifier, + ) { + item { + TextFieldListItem( + placeholder = placeholder.orEmpty(), + text = textFieldContents, + onTextChanged = { + error = if (!validation(it.text)) onValidationErrorMessage else null + textFieldContents = it + }, + error = error, + keyboardOptions = keyboardOptions, + keyboardActions = KeyboardActions(onAny = { + if (validation(textFieldContents.text)) { + onSubmit(textFieldContents.text) + } + }), + maxLines = maxLines, + modifier = Modifier.focusRequester(focusRequester), + ) + } + } + + if (autoSelectOnDisplay) { + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt index abe744bdbc..2eb290dd4e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt @@ -96,6 +96,7 @@ internal fun SimpleAlertDialogContent( thirdButtonText: String? = null, onThirdButtonClicked: () -> Unit = {}, applyPaddingToContents: Boolean = true, + enabled: Boolean = true, icon: @Composable (() -> Unit)? = null, content: @Composable () -> Unit, ) { @@ -122,6 +123,7 @@ internal fun SimpleAlertDialogContent( if (submitText != null) { Button( text = submitText, + enabled = enabled, size = ButtonSize.Medium, onClick = onSubmitClicked, ) 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 121cf26271..e078b634d1 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 @@ -55,4 +55,10 @@ enum class FeatureFlags( description = "Allow user to lock/unlock the app with a pin code or biometrics", defaultValue = false, ), + InRoomCalls( + key = "feature.elementcall", + title = "Element call in rooms", + description = "Allow user to start or join a call in a room", + defaultValue = 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 48f159de83..87a797d13a 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 @@ -37,6 +37,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.NotificationSettings -> true FeatureFlags.VoiceMessages -> false FeatureFlags.PinUnlock -> false + FeatureFlags.InRoomCalls -> false } } else { false diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 5a430f7db5..4a083eacec 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -34,6 +34,7 @@ anvil { } dependencies { + implementation(projects.appconfig) implementation(projects.libraries.di) implementation(libs.dagger) implementation(projects.libraries.core) 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 e352dd5cfc..19e71db332 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,7 +17,7 @@ package io.element.android.libraries.matrix.api.permalink import android.net.Uri -import io.element.android.libraries.matrix.api.config.MatrixConfiguration +import io.element.android.appconfig.MatrixConfiguration /** * Mapping of an input URI to a matrix.to compliant 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 c79ab36a7b..2a388ae580 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,7 +16,7 @@ package io.element.android.libraries.matrix.api.permalink -import io.element.android.libraries.matrix.api.config.MatrixConfiguration +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 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 746f8cead8..4b7e4e4470 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 @@ -30,6 +30,8 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import kotlinx.coroutines.flow.StateFlow import java.io.Closeable import java.io.File @@ -192,5 +194,27 @@ interface MatrixRoom : Closeable { progressCallback: ProgressCallback? ): Result + /** + * Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters. + * @param widgetSettings The widget settings to use. + * @param clientId The client id to use. It should be unique per app install. + * @param languageTag The language tag to use. If null, the default language will be used. + * @param theme The theme to use. If null, the default theme will be used. + * @return The resulting url, or a failure. + */ + suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String? = null, + theme: String? = null, + ): Result + + /** + * Get a [MatrixWidgetDriver] for the provided [widgetSettings]. + * @param widgetSettings The widget settings to use. + * @return The resulting [MatrixWidgetDriver], or a failure. + */ + fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result + override fun close() = destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt new file mode 100644 index 0000000000..f0a22d0128 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.api.widget + +import java.util.UUID + +interface CallWidgetSettingsProvider { + fun provide( + baseUrl: String, + widgetId: String = UUID.randomUUID().toString() + ): MatrixWidgetSettings +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt new file mode 100644 index 0000000000..675adc1ad4 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt @@ -0,0 +1,27 @@ +/* + * 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.api.widget + +import kotlinx.coroutines.flow.Flow + +interface MatrixWidgetDriver : AutoCloseable { + val id: String + val incomingMessages: Flow + + suspend fun run() + suspend fun send(message: String) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetSettings.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetSettings.kt new file mode 100644 index 0000000000..022827898f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetSettings.kt @@ -0,0 +1,29 @@ +/* + * 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.api.widget + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +class MatrixWidgetSettings( + val id: String, + val initAfterContentLoad: Boolean, + val rawUrl: String, +) : Parcelable { + companion object +} 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 4dcdb6d88c..c778fa3ac5 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 @@ -40,6 +40,8 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomNotificationSettings import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.media.map @@ -48,6 +50,8 @@ import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.util.destroyAll +import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver +import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CancellationException @@ -65,6 +69,8 @@ import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle +import org.matrix.rustcomponents.sdk.WidgetPermissions +import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import timber.log.Timber @@ -478,6 +484,27 @@ class RustMatrixRoom( ) } + override suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String?, + theme: String?, + ) = runCatching { + widgetSettings.generateWidgetWebViewUrl(innerRoom, clientId, languageTag, theme) + } + + override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = runCatching { + RustWidgetDriver( + widgetSettings = widgetSettings, + room = innerRoom, + widgetPermissionsProvider = object : WidgetPermissionsProvider { + override fun acquirePermissions(permissions: WidgetPermissions): WidgetPermissions { + return permissions + } + }, + ) + } + private suspend 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/widget/DefaultCallWidgetSettingsProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt new file mode 100644 index 0000000000..a7f208e69d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt @@ -0,0 +1,46 @@ +/* + * 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.widget + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import org.matrix.rustcomponents.sdk.VirtualElementCallWidgetOptions +import org.matrix.rustcomponents.sdk.newVirtualElementCallWidget +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettingsProvider { + override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings { + val options = VirtualElementCallWidgetOptions( + elementCallUrl = baseUrl, + widgetId = widgetId, + parentUrl = null, + hideHeader = null, + preload = null, + fontScale = null, + appPrompt = false, + skipLobby = true, + confineToRoom = true, + fonts = null, + analyticsId = null + ) + val rustWidgetSettings = newVirtualElementCallWidget(options) + return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt new file mode 100644 index 0000000000..65e6c8bc84 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt @@ -0,0 +1,50 @@ +/* + * 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.widget + +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import org.matrix.rustcomponents.sdk.ClientProperties +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.WidgetSettings +import org.matrix.rustcomponents.sdk.generateWebviewUrl + +fun MatrixWidgetSettings.toRustWidgetSettings() = WidgetSettings( + id = this.id, + initAfterContentLoad = this.initAfterContentLoad, + rawUrl = this.rawUrl, +) + +fun MatrixWidgetSettings.Companion.fromRustWidgetSettings(widgetSettings: WidgetSettings) = MatrixWidgetSettings( + id = widgetSettings.id, + initAfterContentLoad = widgetSettings.initAfterContentLoad, + rawUrl = widgetSettings.rawUrl, +) + +suspend fun MatrixWidgetSettings.generateWidgetWebViewUrl( + room: Room, + clientId: String, + languageTag: String? = null, + theme: String? = null +) = generateWebviewUrl( + widgetSettings = this.toRustWidgetSettings(), + room = room, + props = ClientProperties( + clientId = clientId, + languageTag = languageTag, + theme = theme, + ) +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt new file mode 100644 index 0000000000..e385a34e0d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt @@ -0,0 +1,78 @@ +/* + * 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.widget + +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider +import org.matrix.rustcomponents.sdk.makeWidgetDriver +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.coroutineContext + +class RustWidgetDriver( + widgetSettings: MatrixWidgetSettings, + private val room: Room, + private val widgetPermissionsProvider: WidgetPermissionsProvider, +): MatrixWidgetDriver { + + override val incomingMessages = MutableSharedFlow() + + private val driverAndHandle = makeWidgetDriver(widgetSettings.toRustWidgetSettings()) + private var receiveMessageJob: Job? = null + + private var isRunning = AtomicBoolean(false) + + override val id: String = widgetSettings.id + + override suspend fun run() { + // Don't run the driver if it's already running + if (!isRunning.compareAndSet(false, true)) { + return + } + + val coroutineScope = CoroutineScope(coroutineContext) + coroutineScope.launch { + // This call will suspend the coroutine while the driver is running, so it needs to be launched separately + driverAndHandle.driver.run(room, widgetPermissionsProvider) + } + receiveMessageJob = coroutineScope.launch(Dispatchers.IO) { + try { + while (isActive) { + driverAndHandle.handle.recv()?.let { incomingMessages.emit(it) } + } + } finally { + driverAndHandle.handle.close() + } + } + } + + override suspend fun send(message: String) { + driverAndHandle.handle.send(message) + } + + override fun close() { + receiveMessageJob?.cancel() + driverAndHandle.driver.close() + } +} 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 67a36f0db7..2332a37fdc 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 @@ -208,8 +208,12 @@ class FakeMatrixClient( findDmResult = result } - fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom) { - getRoomResults[roomId] = result + fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom?) { + if (result == null) { + getRoomResults.remove(roomId) + } else { + getRoomResults[roomId] = result + } } fun givenSearchUsersResult(searchTerm: String, result: Result) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt new file mode 100644 index 0000000000..80cdcff7ec --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt @@ -0,0 +1,27 @@ +/* + * 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.test + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.SessionId + +class FakeMatrixClientProvider( + private val getClient: (SessionId) -> Result = { Result.success(FakeMatrixClient()) } +) : MatrixClientProvider { + override suspend fun getOrRestore(sessionId: SessionId): Result = getClient(sessionId) +} 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 7549522c0c..c08a742391 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 @@ -36,11 +36,14 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -92,6 +95,8 @@ class FakeMatrixRoom( private var sendPollResponseResult = Result.success(Unit) private var endPollResult = Result.success(Unit) private var progressCallbackValues = emptyList>() + private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io") + private var getWidgetDriverResult: Result = Result.success(FakeWidgetDriver()) val editMessageCalls = mutableListOf>() var sendMediaCount = 0 @@ -368,6 +373,15 @@ class FakeMatrixRoom( progressCallback: ProgressCallback? ): Result = fakeSendMedia(progressCallback) + override suspend fun generateWidgetWebViewUrl( + widgetSettings: MatrixWidgetSettings, + clientId: String, + languageTag: String?, + theme: String?, + ): Result = generateWidgetWebViewUrlResult + + override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = getWidgetDriverResult + fun givenLeaveRoomError(throwable: Throwable?) { this.leaveRoomError = throwable } @@ -475,6 +489,14 @@ class FakeMatrixRoom( fun givenProgressCallbackValues(values: List>) { progressCallbackValues = values } + + fun givenGenerateWidgetWebViewUrlResult(result: Result) { + generateWidgetWebViewUrlResult = result + } + + fun givenGetWidgetDriverResult(result: Result) { + getWidgetDriverResult = result + } } data class SendLocationInvocation( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt new file mode 100644 index 0000000000..74cf94e4ad --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeCallWidgetSettingsProvider.kt @@ -0,0 +1,32 @@ +/* + * 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.test.widget + +import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider +import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings + +class FakeCallWidgetSettingsProvider( + private val provideFn: (String, String) -> MatrixWidgetSettings = { _, _ -> MatrixWidgetSettings("id", true, "url") } +) : CallWidgetSettingsProvider { + + val providedBaseUrls = mutableListOf() + + override fun provide(baseUrl: String, widgetId: String): MatrixWidgetSettings { + providedBaseUrls += baseUrl + return provideFn(baseUrl, widgetId) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeWidgetDriver.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeWidgetDriver.kt new file mode 100644 index 0000000000..f7fa2b494a --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/widget/FakeWidgetDriver.kt @@ -0,0 +1,52 @@ +/* + * 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.test.widget + +import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver +import kotlinx.coroutines.flow.MutableSharedFlow +import java.util.UUID + +class FakeWidgetDriver( + override val id: String = UUID.randomUUID().toString(), +) : MatrixWidgetDriver { + + private val _sentMessages = mutableListOf() + val sentMessages: List = _sentMessages + + var runCalledCount = 0 + private set + var closeCalledCount = 0 + private set + + override val incomingMessages = MutableSharedFlow(extraBufferCapacity = 1) + + override suspend fun run() { + runCalledCount++ + } + + override suspend fun send(message: String) { + _sentMessages.add(message) + } + + override fun close() { + closeCalledCount++ + } + + fun givenIncomingMessage(message: String) { + incomingMessages.tryEmit(message) + } +} diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt index 8ad2c098f6..d62fb7e6cf 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt @@ -25,5 +25,8 @@ interface PreferencesStore { suspend fun setDeveloperModeEnabled(enabled: Boolean) fun isDeveloperModeEnabledFlow(): Flow + suspend fun setCustomElementCallBaseUrl(string: String?) + fun getCustomElementCallBaseUrlFlow(): Flow + suspend fun reset() } diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt index 337301f23e..66a46d1ca3 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt @@ -21,6 +21,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.preferences.api.store.PreferencesStore @@ -37,6 +38,7 @@ private val Context.dataStore: DataStore by preferencesDataStore(na private val richTextEditorKey = booleanPreferencesKey("richTextEditor") private val developerModeKey = booleanPreferencesKey("developerMode") +private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl") @ContributesBinding(AppScope::class) class DefaultPreferencesStore @Inject constructor( @@ -71,6 +73,22 @@ class DefaultPreferencesStore @Inject constructor( } } + override suspend fun setCustomElementCallBaseUrl(string: String?) { + store.edit { prefs -> + if (string != null) { + prefs[customElementCallBaseUrlKey] = string + } else { + prefs.remove(customElementCallBaseUrlKey) + } + } + } + + override fun getCustomElementCallBaseUrlFlow(): Flow { + return store.data.map { prefs -> + prefs[customElementCallBaseUrlKey] + } + } + override suspend fun reset() { store.edit { it.clear() } } diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt index a2a9fdaa3f..6dea8910ed 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt @@ -23,9 +23,11 @@ import kotlinx.coroutines.flow.MutableStateFlow class InMemoryPreferencesStore( isRichTextEditorEnabled: Boolean = false, isDeveloperModeEnabled: Boolean = false, + customElementCallBaseUrl: String? = null, ) : PreferencesStore { private var _isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled) private var _isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) + private var _customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) override suspend fun setRichTextEditorEnabled(enabled: Boolean) { _isRichTextEditorEnabled.value = enabled @@ -43,6 +45,14 @@ class InMemoryPreferencesStore( return _isDeveloperModeEnabled } + override suspend fun setCustomElementCallBaseUrl(string: String?) { + _customElementCallBaseUrl.tryEmit(string) + } + + override fun getCustomElementCallBaseUrlFlow(): Flow { + return _customElementCallBaseUrl + } + override suspend fun reset() { // No op } 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 f28095763c..7ada336f28 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -131,7 +131,6 @@ "např. název vašeho projektu" "Hledat někoho" "Výsledky hledání" - "Zabezpečená záloha" "Zabezpečení" "Odesílání…" "Server není podporován" @@ -208,9 +207,9 @@ "Další nastavení" "Halsové a video hovory" "Neshoda konfigurace" - "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. + "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. -Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. +Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. Pokud budete pokračovat, některá nastavení se mohou změnit." "Přímé zprávy" 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 7401685482..ada42316b1 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -126,7 +126,6 @@ "например, название вашего проекта" "Поиск человека" "Результаты поиска" - "Безопасное резервное копирование" "Безопасность" "Отправка…" "Сервер не поддерживается" 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 536db8c501..c6cf324b75 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -131,7 +131,6 @@ "napr. názov vášho projektu" "Vyhľadať niekoho" "Výsledky hľadania" - "Bezpečné zálohovanie" "Bezpečnosť" "Odosiela sa…" "Server nie je podporovaný" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index b9a6460bec..1ff0ac1bc0 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -69,6 +69,8 @@ "Share" "Share link" "Sign in again" + "Sign out" + "Sign out anyway" "Skip" "Start" "Start chat" @@ -84,6 +86,7 @@ "Analytics" "Audio" "Bubbles" + "Chat backup" "Copyright" "Creating room…" "Left room" @@ -122,6 +125,7 @@ "Privacy policy" "Reaction" "Reactions" + "Recovery key" "Refreshing…" "Replying to %1$s" "Report a bug" @@ -131,7 +135,6 @@ "e.g. your project name" "Search for someone" "Search results" - "Secure backup" "Security" "Sending…" "Server not supported" @@ -200,6 +203,22 @@ "This is the beginning of this conversation." "New" "Share analytics data" + "Turn off backup" + "Turn on backup" + "Backup ensures that you don\'t lose your message history." + "Backup" + "Change recovery key" + "Confirm recovery key" + "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$@ everywhere." + "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?" + "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:" + "Not have encrypted message history on new devices" + "Lose access to your encrypted messages if you are signed out of %1$@ everywhere" + "Are you sure you want to turn off backup?" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." @@ -228,6 +247,27 @@ If you proceed, some of your settings may change." "system settings" "System notifications turned off" "Notifications" + "Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work." + "Generate a new recovery key" + "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." + "Enter the 48 character code." + "Enter…" + "Recovery key confirmed" + "Confirm your recovery key" + "Save recovery key" + "Write down your recovery key somewhere safe or save it in a password manager." + "Tap to copy recovery key" + "Save your recovery key" + "You will not be able to access your new recovery key after this step." + "Have you saved your recovery key?" + "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’." + "Generate your recovery key" + "Make sure you can store your recovery key somewhere safe" + "Recovery setup successful" + "Set up recovery" "Check if you want to hide all current and future messages from this user" "Share location" "Share my location" diff --git a/settings.gradle.kts b/settings.gradle.kts index 105befcd04..9ac2c96dde 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -52,6 +52,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "ElementX" include(":app") include(":appnav") +include(":appconfig") include(":tests:konsist") include(":tests:uitests") include(":tests:testutils") diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f44d866ff8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a28b7969455f17784f060291ce58b3720324baa67e9d93c2aa59f6d979268678 +size 14429 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..232a3e752a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.ui_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8a811677a50035a361f65ece4a1281346423226db6a1b8b3b8611f6b2f1d23d +size 13099 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 4e99b44510..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-D-0_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0264d691ec2946cda4d5860f02079dd9f3e69ddd30a2e5c2f9c701253fd659c -size 10499 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 5301f939c3..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call_null_CallScreenView-N-0_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20ec46c4c66a68d93c45a17eafd945536c9c137b18e66a82e83eced674708d98 -size 9732 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png index 72056b5ed9..6f1f867a7c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88bdef3999877e5017bfe0e0ead1514e4e6a58abcde0b0167d4b0ad9d4abd1e0 -size 54020 +oid sha256:5f219bd9b9363f237e15bb73655dd53b2ec143e18c8544c11efbfa90390e091c +size 54312 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png index 90b4f4652f..062dfb77f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35f420b550029d7f8b22d73ea0349d2794cc2e5c5f3080799f496774fad7d2ff -size 55440 +oid sha256:6065f5330e1b3638719c6743db098bf6576fcffc6ccf8215f7f600a0b981144b +size 55731 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png index c99c22634e..1de7fbc725 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9518f3e4809856f3787bcd076f1a4f33067ea911e66cd6692790451309e6e192 -size 55769 +oid sha256:f4bdafdfa50665f05ba5cd8749252e7e5d65bea8a8c6bfc1c46eb1acd1570b52 +size 56086 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png index 1ae57f1414..d348a6f13f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f62a8a4eded0b742e911970837fe1003228834dd07a216a9bdc4acef37aa468 -size 55800 +oid sha256:2bfcc4dcaa8980cfc9c724e64a93116ac1fb81a1bf4421532cb196d4e07db7c5 +size 56024 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png index 31edf89ccf..224cef4b67 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1314aaf5394d03b5d08eccdb29783ce8d949f44d3eea28ec8ce434b830515304 -size 51662 +oid sha256:c86a6da3d45b767a41f65e0cf4e8cacefb33e0409386a6233140891deb2258f6 +size 51965 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png index 617d8da89b..82636b3e21 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecdc26ae1b8943734a8ec1020d7ca9ad4a8e570f5295eb568f80ed314585a9a9 -size 51981 +oid sha256:5a1fc20759ff45eeef547621853b9684a663fe9dcdf78b316454dcae26d078f9 +size 52286 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png index 58b944edce..094ab2536a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32ae9c61f8a01bd54b9fb51af5f0dff222f4df0be1003a9c6ad680d20877448b -size 52275 +oid sha256:7b9bc4654b6911d7f50b4b0242b650a529cb22c13a58caf5eae50366a2d27b37 +size 52532 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png index e28209550e..32de8d07f9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1785f0fe49a5afd9b152f6ddb51acad7ba1700b2711cf302ef3a490d948fcd96 -size 53618 +oid sha256:0e051668beb9030ff184d6e75831a00202854f6448880f399a7ee2d5be187740 +size 53880 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png index 5113ea50e1..009ddeadaa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8907587eb26b29d273fcce14fe4f17acd3ab8ae2fffa83d2c5d5c2c0d8c29bc6 -size 54270 +oid sha256:c6c6603452db218811f3f5a2baa934ca640755b969e81ca8cbf6b3dcf663aee9 +size 54551 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png index 6162f39468..d2f2f56d9b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:455b414e5da5d5174a8d87ef37219c9754e3558b39f025d7aab226b041c51096 -size 51305 +oid sha256:e953bc91e9959f1635d12416f6afde79427aeac4edef44fb38f1512672e544bd +size 51552 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png index 3b7c0855e3..9437874ffd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0b83b1d37b34cdf0769e83f625d66479638ce402d5c8e76ef40548414d34400 -size 49862 +oid sha256:0157713ae934c0771a5d3d4d064ce6dc9be11b1d8011f2360740d7f4a20f6f04 +size 50162 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png index 500b83a53e..af4dae6214 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b09d4a60c3d9944cd6d50a8f0a06d5b3522eba7884f3b5e1e6889f93e8dd1794 -size 50026 +oid sha256:24f1d30bb62ed620671e87062a338fd53f409bce6e08fb7b116249decf74e409 +size 50295 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2c48d3e55d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4e1af15c571d1f087005849b627d79387f8f5557bbc4233768bb3c2d940d628 +size 48510 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..519d9d4d10 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_null_AdvancedSettingsView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa25ebf20fe62af56a548c3e962ae2e76e6e8e1b7e685d021306b733613e49eb +size 45462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_TextfieldListitem-textfieldvalue_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_TextfieldListitem-textfieldvalue_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c7b2473e20 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.list_null_Listitems_TextfieldListitem-textfieldvalue_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17fa588278f61269982bda072f4a67f9cc6f39f6f1ebcf4da59c7c3006808e14 +size 11262 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 7e07d269c0..0525eb834d 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -145,6 +145,7 @@ "name": ":features:preferences:impl", "includeRegex": [ "screen_advanced_settings_.*", + "screen\\.advanced_settings\\..*", "screen_edit_profile_.*" ] }, From aa78255b4bfde9d0045d8acef2a491f9ca0533ef Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 19 Oct 2023 15:47:25 +0000 Subject: [PATCH 084/281] Update screenshots --- ...ultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ull_EditUserProfileView-D-10_10_null_0,NEXUS_5,1.0,en].png} | 0 ...ull_EditUserProfileView-N-10_11_null_0,NEXUS_5,1.0,en].png} | 0 ...user_null_UserPreferences-D-9_9_null_0,NEXUS_5,1.0,en].png} | 0 ...user_null_UserPreferences-D-9_9_null_1,NEXUS_5,1.0,en].png} | 0 ...user_null_UserPreferences-D-9_9_null_2,NEXUS_5,1.0,en].png} | 0 ...ser_null_UserPreferences-N-9_10_null_0,NEXUS_5,1.0,en].png} | 0 ...ser_null_UserPreferences-N-9_10_null_1,NEXUS_5,1.0,en].png} | 0 ...ser_null_UserPreferences-N-9_10_null_2,NEXUS_5,1.0,en].png} | 0 ...edRoomNotificationSettings-D-5_5_null_0,NEXUS_5,1.0,en].png | 3 +++ ...edRoomNotificationSettings-N-5_6_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ts.avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png | 3 +++ ...ts.avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png | 3 +++ ...ts.avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png | 3 +++ 15 files changed, 21 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-9_9_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-10_10_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-9_10_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-10_11_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.preferences.impl.user_null_UserPreferences-D-8_8_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_9_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.preferences.impl.user_null_UserPreferences-D-8_8_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_9_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.preferences.impl.user_null_UserPreferences-D-8_8_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_9_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.preferences.impl.user_null_UserPreferences-N-8_9_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.preferences.impl.user_null_UserPreferences-N-9_10_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.preferences.impl.user_null_UserPreferences-N-8_9_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.preferences.impl.user_null_UserPreferences-N-9_10_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.preferences.impl.user_null_UserPreferences-N-8_9_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.preferences.impl.user_null_UserPreferences-N-9_10_null_2,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_UserDefinedRoomNotificationSettings-D-5_5_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_UserDefinedRoomNotificationSettings-N-5_6_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1d4750eecb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1172d81259b7e7b694f428c5144d96c8a7748134f5b4f515a5c25c58ff8e5e1b +size 31318 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c423fe4456 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e835ab12c0a77c4a4dcaf8bac0d78ed5ec9a0ab8eef669c7d814317ab06abd27 +size 28443 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-9_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-10_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.editprofile_null_EditUserProfileView-D-9_9_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-D-10_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.editprofile_null_EditUserProfileView-N-9_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-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_null_EditUserProfileView-N-9_10_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user.editprofile_null_EditUserProfileView-N-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_null_UserPreferences-D-8_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_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.user_null_UserPreferences-D-8_8_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-D-8_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_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.user_null_UserPreferences-D-8_8_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-D-8_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_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.user_null_UserPreferences-D-8_8_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-D-9_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-N-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-N-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_null_UserPreferences-N-8_9_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-N-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_null_UserPreferences-N-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-N-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_null_UserPreferences-N-8_9_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-N-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_null_UserPreferences-N-8_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-N-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_null_UserPreferences-N-8_9_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.user_null_UserPreferences-N-9_10_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_UserDefinedRoomNotificationSettings-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_UserDefinedRoomNotificationSettings-D-5_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d26299f528 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_UserDefinedRoomNotificationSettings-D-5_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:372ff5eca3c8c9de6bb15d5ae9a55f1d8ec124c7e454d411be6bd28f78d3aed0 +size 24324 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_UserDefinedRoomNotificationSettings-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_UserDefinedRoomNotificationSettings-N-5_6_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f8beb00ae9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_UserDefinedRoomNotificationSettings-N-5_6_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3795832f75ba48402f228dbd5be5dc05b10dab00242922fb861c0f77fcd391e4 +size 22853 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a3d0076294 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_48,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2619a9a70eca12d5931ebc21adce48a1aed8383f35908bb09546f44b40f04543 +size 23094 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..81b9668b0f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_49,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d7d71d58b250bdec2d9b6e1ed46c5e3ffd98ffeefcaf4267b9979970a89750b +size 22226 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5cb8c0a0bf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.avatar_null_Avatars_Avatar_0_null_50,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7bdea3caef3f1be9fac1fbb5511fe2d76b7985576c038f8c1d920615c3d49cd +size 25005 From f1cf9b6e310f923effa8138f3d659dd7459d1cc7 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 19 Oct 2023 16:57:35 +0100 Subject: [PATCH 085/281] Fix typo and preview --- .../edit/EditDefaultNotificationSettingStateProvider.kt | 1 + .../android/features/roomdetails/impl/RoomDetailsFlowNode.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt index 3446c24efe..2b1236f4af 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt @@ -47,5 +47,6 @@ private fun aRoomSummary() = RoomSummary.Filled( lastMessage = null, lastMessageTimestamp = null, unreadNotificationCount = 0, + notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, ) ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 5d7539e626..6945ebb0ca 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -70,7 +70,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data class RoomNotificationSettings( /** - * When presented from oursite the context of the room, the rooms settings UI is different. + * When presented from outsite the context of the room, the rooms settings UI is different. * Figma designs: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=5199-198932&mode=design&t=fTTvpuxYFjewYQOe-0 */ val showUserDefinedSettingStyle: Boolean From 85ae33a0fde209271a73cb478a731158e5903c84 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 19 Oct 2023 16:08:50 +0000 Subject: [PATCH 086/281] Update screenshots --- ...ltNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ltNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png index 1d4750eecb..7e1e8e88a0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1172d81259b7e7b694f428c5144d96c8a7748134f5b4f515a5c25c58ff8e5e1b -size 31318 +oid sha256:11aa649bb8e25975c79ebc8c20aa82153acac75b4087f99485c58bd604ba3f33 +size 35947 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png index c423fe4456..6e3219983b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e835ab12c0a77c4a4dcaf8bac0d78ed5ec9a0ab8eef669c7d814317ab06abd27 -size 28443 +oid sha256:1df0f8620db5a3f751e37543f34785517703e4731af9e624dc50b91de79f46e9 +size 33107 From 784415f6986ac3c0823aee8658c93e8d05d45464 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 21:58:12 +0200 Subject: [PATCH 087/281] Pin auth : simple first iteration on ui --- .../impl/auth/PinAuthenticationView.kt | 115 +++++++++-- .../lockscreen/impl/auth/numpad/PinKeypad.kt | 182 ++++++++++++++++++ .../impl/auth/numpad/PinKeypadModel.kt | 26 +++ 3 files changed, 304 insertions(+), 19 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt index 2b62e46800..740d85adec 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt @@ -16,22 +16,40 @@ package io.element.android.features.lockscreen.impl.auth -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.features.lockscreen.impl.auth.numpad.PinKeypad import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage 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.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme @Composable fun PinAuthenticationView( @@ -40,27 +58,23 @@ fun PinAuthenticationView( ) { Surface(modifier) { HeaderFooterPage( - modifier = Modifier - .systemBarsPadding() - .fillMaxSize(), header = { PinAuthenticationHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, - footer = { PinAuthenticationFooter(state) }, + content = { + Box( + modifier = Modifier + .padding(top = 40.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + PinKeypad( + onClick = {} + ) + } + } ) } } -@Composable -private fun PinAuthenticationHeader( - modifier: Modifier = Modifier, -) { - IconTitleSubtitleMolecule( - modifier = modifier, - title = "Element X is locked", - subTitle = null, - iconImageVector = Icons.Default.Lock, - ) -} - @Composable private fun PinAuthenticationFooter(state: PinAuthenticationState) { Button( @@ -72,6 +86,69 @@ private fun PinAuthenticationFooter(state: PinAuthenticationState) { ) } +@Composable +private fun PinDotsRow( + modifier: Modifier = Modifier, +) { + Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + PinDot(isFilled = true) + PinDot(isFilled = true) + PinDot(isFilled = false) + PinDot(isFilled = false) + } +} + +@Composable +private fun PinDot( + isFilled: Boolean, + modifier: Modifier = Modifier, +) { + val backgroundColor = if (isFilled) { + ElementTheme.colors.iconPrimary + } else { + ElementTheme.colors.bgSubtlePrimary + } + Box( + modifier = modifier + .size(14.dp) + .background(backgroundColor, CircleShape) + ) +} + +@Composable +private fun PinAuthenticationHeader( + modifier: Modifier = Modifier, +) { + Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + modifier = Modifier + .size(32.dp), + tint = ElementTheme.colors.iconPrimary, + imageVector = Icons.Filled.Lock, + contentDescription = "", + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Enter your PIN", + modifier = Modifier + .fillMaxWidth(), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontHeadingMdBold, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = "You have 3 attempts to unlock", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.secondary, + ) + Spacer(Modifier.height(24.dp)) + PinDotsRow() + } +} + @Composable @PreviewsDayNight internal fun PinAuthenticationViewPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt new file mode 100644 index 0000000000..43abc24002 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt @@ -0,0 +1,182 @@ +/* + * 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.lockscreen.impl.auth.numpad + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Backspace +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.toSp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun PinKeypad( + onClick: (PinKeypadModel) -> Unit, + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + verticalAlignment: Alignment.Vertical = Alignment.Top, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, +) { + Column( + modifier = modifier, + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + ) { + PinKeypadRow( + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = listOf(PinKeypadModel.Number("1"), PinKeypadModel.Number("2"), PinKeypadModel.Number("3")), + onClick = onClick, + ) + PinKeypadRow( + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = listOf(PinKeypadModel.Number("4"), PinKeypadModel.Number("5"), PinKeypadModel.Number("6")), + onClick = onClick, + ) + PinKeypadRow( + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = listOf(PinKeypadModel.Number("7"), PinKeypadModel.Number("8"), PinKeypadModel.Number("9")), + onClick = onClick, + ) + PinKeypadRow( + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number("0"), PinKeypadModel.Back), + onClick = onClick, + ) + } +} + +@Composable +private fun PinKeypadRow( + models: List, + onClick: (PinKeypadModel) -> Unit, + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, +) { + Row( + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, + modifier = modifier, + ) { + val commonModifier = Modifier.size(80.dp) + for (model in models) { + when (model) { + is PinKeypadModel.Empty -> { + Spacer(modifier = commonModifier) + } + is PinKeypadModel.Back -> { + PinKeypadBackButton( + modifier = commonModifier, + onClick = { onClick(model) }, + ) + } + is PinKeypadModel.Number -> { + PinKeyBadDigitButton( + size = 80.dp, + modifier = commonModifier, + digit = model.number, + onClick = { onClick(model) }, + ) + } + } + } + } +} + +@Composable +private fun PinKeyBadDigitButton( + digit: String, + size: Dp, + onClick: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Button( + colors = ButtonDefaults.buttonColors( + containerColor = ElementTheme.colors.bgSubtlePrimary, + contentColor = Color.Transparent, + ), + shape = CircleShape, + contentPadding = PaddingValues(0.dp), + modifier = modifier + .clip(CircleShape), + onClick = { onClick(digit) } + ) { + val fontSize = 80.dp.toSp() / 2 + val originalFont = ElementTheme.typography.fontHeadingXlBold + val ratio = fontSize.value / originalFont.fontSize.value + val lineHeight = originalFont.lineHeight * ratio + Text( + text = digit, + color = ElementTheme.colors.textPrimary, + style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp), + ) + } +} + +@Composable +private fun PinKeypadBackButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .clip(CircleShape) + .background(color = Color.Transparent, shape = CircleShape), + onClick = onClick, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Backspace, + contentDescription = null, + ) + } +} + +@Composable +@PreviewsDayNight +fun PinKeypad() { + ElementPreview { + PinKeypad(onClick = {}) + } +} + + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt new file mode 100644 index 0000000000..108486e400 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt @@ -0,0 +1,26 @@ +/* + * 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.lockscreen.impl.auth.numpad + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface PinKeypadModel { + data object Empty : PinKeypadModel + data object Back : PinKeypadModel + data class Number(val number: String) : PinKeypadModel +} From b4173fbe467be9e0880c1f3f4c51a4cc95a3ac40 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Oct 2023 22:23:19 +0200 Subject: [PATCH 088/281] Create pin : fix some spacing --- .../lockscreen/impl/create/CreatePinView.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index 4e01de8c2f..cddadbbae9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -25,12 +25,16 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets +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.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api @@ -49,7 +53,6 @@ import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure 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.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview @@ -77,13 +80,18 @@ fun CreatePinView( ) }, content = { padding -> - HeaderFooterPage( + val scrollState = rememberScrollState() + Column( modifier = Modifier + .imePadding() .padding(padding) - .consumeWindowInsets(padding), - header = { CreatePinHeader(state.isConfirmationStep, state.pinSize) }, - content = { CreatePinContent(state) } - ) + .consumeWindowInsets(padding) + .verticalScroll(state = scrollState) + .padding(vertical = 16.dp, horizontal = 20.dp), + ) { + CreatePinHeader(state.isConfirmationStep, state.pinSize) + CreatePinContent(state) + } } ) } @@ -99,7 +107,6 @@ private fun CreatePinHeader( horizontalAlignment = Alignment.CenterHorizontally, ) { IconTitleSubtitleMolecule( - modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = if (isValidationStep) { stringResource(id = R.string.screen_app_lock_setup_confirm_pin) } else { @@ -110,7 +117,7 @@ private fun CreatePinHeader( ) Text( text = stringResource(id = R.string.screen_app_lock_setup_pin_context_warning), - modifier = Modifier.padding(8.dp), + modifier = Modifier.padding(top = 24.dp), textAlign = TextAlign.Center, style = ElementTheme.typography.fontBodyMdRegular, color = MaterialTheme.colorScheme.secondary, From a0cde93b1df5b3e3972002a2ccfc06e0b0c2147b Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 19 Oct 2023 20:39:07 +0000 Subject: [PATCH 089/281] Update screenshots --- ...create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png index f110f617f8..6d36e3b71b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e2d58ea747cf87fd6e4f70b3ebb1f1e638a4ec3f7ac8cb712566a889167c3e1 -size 35045 +oid sha256:8ddeca63feb6f81e2db0da909657f1f90cfb13facb8ce854cc79e4eb33e329ee +size 34635 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png index e5a3fe28af..3981b8e841 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4be560206e4fc9101d8a793ad5f5f9cc2057fb47e60f37f91ce6dae8e2b995e2 -size 35147 +oid sha256:28dbde9da91754c0377667bc852b8df3f17d2c6d91f0be4b04a5762cda7995e6 +size 34703 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png index 87cefa723f..a054b68cf1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da83f8b634c8217636b08792498704d9ffc1623c8d4aedfb15265d2f5e085e86 -size 32805 +oid sha256:e06d34c115e1e3ecadf73a78701f8e565f0bc9cd38d1eb3f007c0ec4486cbf3e +size 32319 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png index d3d3ba4038..fede94e2a3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:598214e07c2545ac4dd061f6237a737e91e9c47eeea01a63ab064b3398e39b68 -size 24046 +oid sha256:9368e0cae34cde6e8559702c8de4cb42d684cea5f13cbd62d9ab0b7de918e684 +size 28624 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png index 7476004b1f..9f40f229df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ec4a6f05bfb15e018c19205cb919f6619440e25392ed0a74fb2896ddb1decb6 -size 28266 +oid sha256:3a948fedb7df6d6cd25793d9aab2a96487bd1a7bd31ba8bacb44026d0c97582c +size 36925 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png index 41a349426e..e1cff5f4ef 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7c84953e8c1c3b2384078be4ca8cd5c763c032bfa22bb2ba034dcbee8ef9c72 -size 33594 +oid sha256:e17eaf4585825f04fb121296d4b1370d652fc64d28149c285ac28849267ea9f0 +size 33051 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png index 8f6c3fdbba..b67c6d82ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0917206ffe964311097f05de3fb838e0cc23f6fdbb2cd106d3015a751b378cfc -size 33864 +oid sha256:9b0b7a756d5195a5397ef4eeba5da8b1cbdc8a4a182e935d03ef9d258d921b5f +size 33278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png index 64bc9a11cd..f6c53f382a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32b2c6514d9a23da09c932e198f29ec112b5afa872a0fa42cd5de85689660891 -size 31658 +oid sha256:d42ed3cb545fc96e1840db73c8efdc6782a545572539855ccceceda50663f284 +size 31445 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png index 6d10aeb87d..2047696c67 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bdedf444797479efa73d6ca7d3d6545f4bd01faa80d50980b0ad6ea427b09ee -size 20828 +oid sha256:75785d23eafb006cbfbbab3e6824364a419f56d0a7d2d2a734b6f0947ca7843b +size 25358 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png index 7c2736059e..21e905b2df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51774321b68ee4d1f5a605edf1ce630e0f7651191e93cab6a885fc90f1639061 -size 24493 +oid sha256:624d62ed7dd52617d8c02bcf5be4ded2707500e930d56f4362c834226e6108bd +size 32514 From 0664f01c89eb3a83843b53fabd59f129237eef87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 08:56:10 +0200 Subject: [PATCH 090/281] Update dependency org.jsoup:jsoup to v1.16.2 (#1613) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b2e91fce99..9c7e956569 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,7 +38,7 @@ coil = "2.4.0" datetime = "0.4.1" serialization_json = "1.6.0" showkase = "1.0.0-beta18" -jsoup = "1.16.1" +jsoup = "1.16.2" appyx = "1.4.0" dependencycheck = "8.4.0" dependencyanalysis = "1.25.0" From 510bdd47afcfc742313cbb8ae63a4f90b83ffd85 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 10:41:39 +0200 Subject: [PATCH 091/281] Remove AppNameProvider, we have buildMeta now. --- .../libraries/push/impl/PushersManager.kt | 6 +-- .../toolbox/api/appname/AppNameProvider.kt | 21 --------- .../impl/appname/DefaultAppNameProvider.kt | 47 ------------------- 3 files changed, 3 insertions(+), 71 deletions(-) delete mode 100644 services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt delete mode 100644 services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt 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 7f4ee63004..b49f8f4299 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag +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 @@ -28,7 +29,6 @@ import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyReque 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 -import io.element.android.services.toolbox.api.appname.AppNameProvider import timber.log.Timber import javax.inject.Inject @@ -39,7 +39,7 @@ private val loggerTag = LoggerTag("PushersManager", LoggerTag.PushLoggerTag) @ContributesBinding(AppScope::class) class PushersManager @Inject constructor( // private val localeProvider: LocaleProvider, - private val appNameProvider: AppNameProvider, + private val buildMeta: BuildMeta, // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, private val pushClientSecret: PushClientSecret, @@ -88,7 +88,7 @@ class PushersManager @Inject constructor( appId = PushConfig.pusher_app_id, profileTag = DEFAULT_PUSHER_FILE_TAG + "_" /* TODO + abs(activeSessionHolder.getActiveSession().myUserId.hashCode())*/, lang = "en", // TODO localeProvider.current().language, - appDisplayName = appNameProvider.getAppName(), + appDisplayName = buildMeta.applicationName, deviceDisplayName = "MyDevice", // TODO getDeviceInfoUseCase.execute().displayName().orEmpty(), url = gateway, defaultPayload = createDefaultPayload(pushClientSecret.getSecretForUser(userId)) diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt deleted file mode 100644 index 414c9b632e..0000000000 --- a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt +++ /dev/null @@ -1,21 +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.services.toolbox.api.appname - -interface AppNameProvider { - fun getAppName(): String -} diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt deleted file mode 100644 index 7a5cbd46f0..0000000000 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt +++ /dev/null @@ -1,47 +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.services.toolbox.impl.appname - -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.services.toolbox.api.appname.AppNameProvider -import timber.log.Timber -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultAppNameProvider @Inject constructor(@ApplicationContext private val context: Context) : - AppNameProvider { - - override fun getAppName(): String { - return try { - val appPackageName = context.packageName - var appName = context.getApplicationLabel(appPackageName) - - // Use appPackageName instead of appName if appName contains any non-ASCII character - if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { - appName = appPackageName - } - appName - } catch (e: Exception) { - Timber.e(e, "## AppNameProvider() : failed") - "ElementAndroid" - } - } -} From 12f5ee5b1b138f3c2654aaf9ecc627167db31d61 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 10:52:13 +0200 Subject: [PATCH 092/281] Fix small issue in the script. --- tools/sdk/build_rust_sdk.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sdk/build_rust_sdk.sh b/tools/sdk/build_rust_sdk.sh index 6853642218..3d7dc17059 100755 --- a/tools/sdk/build_rust_sdk.sh +++ b/tools/sdk/build_rust_sdk.sh @@ -64,7 +64,7 @@ printf "\nBuilding the SDK for aarch64-linux-android...\n\n" cd ../element-x-android mv ./libraries/rustsdk/sdk-android-debug.aar ./libraries/rustsdk/matrix-rust-sdk.aar mkdir -p ./libraries/rustsdk/sdks -cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/matrix-rust-sdk-${date}.aar +cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/sdks/matrix-rust-sdk-${date}.aar if [ ${buildApp} == "yes" ]; then From 0df609cef5f7b56fefa4dde469a0877414fee08b Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 10:54:59 +0200 Subject: [PATCH 093/281] Create PIN : hopefully fix remaining issues --- .../impl/create/CreatePinPresenter.kt | 3 +++ .../lockscreen/impl/create/CreatePinState.kt | 1 + .../impl/create/CreatePinStateProvider.kt | 1 + .../lockscreen/impl/create/CreatePinView.kt | 18 ++++-------------- .../impl/src/main/res/values/localazy.xml | 7 ++++--- .../impl/create/CreatePinPresenterTest.kt | 3 ++- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt index e72e636ed4..957594c0f7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt @@ -25,12 +25,14 @@ import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.create.validation.PinValidator import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta import javax.inject.Inject private const val PIN_SIZE = 4 class CreatePinPresenter @Inject constructor( private val pinValidator: PinValidator, + private val buildMeta: BuildMeta, ) : Presenter { @Composable @@ -94,6 +96,7 @@ class CreatePinPresenter @Inject constructor( confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, createPinFailure = createPinFailure, + appName = buildMeta.applicationName, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt index 5bb632f04e..020076a2ab 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt @@ -24,6 +24,7 @@ data class CreatePinState( val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, val createPinFailure: CreatePinFailure?, + val appName: String, val eventSink: (CreatePinEvents) -> Unit ) { val pinSize = choosePinEntry.size diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt index 543360f91e..c9dcce018d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt @@ -55,6 +55,7 @@ fun aCreatePinState( confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, createPinFailure = creationFailure, + appName = "Element", eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt index cddadbbae9..063d65f41f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding @@ -38,14 +37,12 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R @@ -89,7 +86,7 @@ fun CreatePinView( .verticalScroll(state = scrollState) .padding(vertical = 16.dp, horizontal = 20.dp), ) { - CreatePinHeader(state.isConfirmationStep, state.pinSize) + CreatePinHeader(state.isConfirmationStep, state.appName) CreatePinContent(state) } } @@ -99,7 +96,7 @@ fun CreatePinView( @Composable private fun CreatePinHeader( isValidationStep: Boolean, - pinSize: Int, + appName: String, modifier: Modifier = Modifier, ) { Column( @@ -110,18 +107,11 @@ private fun CreatePinHeader( title = if (isValidationStep) { stringResource(id = R.string.screen_app_lock_setup_confirm_pin) } else { - stringResource(id = R.string.screen_app_lock_setup_choose_pin, pinSize) + stringResource(id = R.string.screen_app_lock_setup_choose_pin) }, - subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context), + subTitle = stringResource(id = R.string.screen_app_lock_setup_pin_context, appName), iconImageVector = Icons.Filled.Lock, ) - Text( - text = stringResource(id = R.string.screen_app_lock_setup_pin_context_warning), - modifier = Modifier.padding(top = 24.dp), - textAlign = TextAlign.Center, - style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, - ) } } diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml index fb5c2be73c..6b12eac427 100644 --- a/features/lockscreen/impl/src/main/res/values/localazy.xml +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -10,12 +10,13 @@ "Remove PIN" "Are you sure you want to remove PIN?" "Remove PIN?" - "Choose %1$d digit PIN" + "Choose PIN" "Confirm PIN" "You cannot choose this as your PIN code for security reasons" "Choose a different PIN" - "Lock Element to add extra security to your chats." - "Choose something memorable. If you forget this PIN, you will be logged out of the app." + "Lock %1$s to add extra security to your chats. + +Choose something memorable. If you forget this PIN, you will be logged out of the app." "Please enter the same PIN twice" "PINs don\'t match" "You’ll need to re-login and create a new PIN to proceed" diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt index c1af14f519..78536bb693 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.lockscreen.impl.create.model.PinDigit import io.element.android.features.lockscreen.impl.create.model.PinEntry import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest import org.junit.Test @@ -108,6 +109,6 @@ class CreatePinPresenterTest { } private fun createCreatePinPresenter(): CreatePinPresenter { - return CreatePinPresenter(PinValidator()) + return CreatePinPresenter(PinValidator(), aBuildMeta()) } } From 432768923422ef20aaaa8ad0ddd77321a54d63d6 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 20 Oct 2023 09:12:23 +0000 Subject: [PATCH 094/281] Update screenshots --- ...create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png index 6d36e3b71b..c3d918b1c2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ddeca63feb6f81e2db0da909657f1f90cfb13facb8ce854cc79e4eb33e329ee -size 34635 +oid sha256:c9f1f9f900cea024cdf777867c33c1d1bbe4744a5b91cd994a03aa6cbf4f8815 +size 33186 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png index 3981b8e841..0c72d54e99 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28dbde9da91754c0377667bc852b8df3f17d2c6d91f0be4b04a5762cda7995e6 -size 34703 +oid sha256:d25ab8d3f53a5139b265a1ddef43ef3755b539a695aa8f43615e9197bd93471d +size 33231 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png index a054b68cf1..61c45c675c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e06d34c115e1e3ecadf73a78701f8e565f0bc9cd38d1eb3f007c0ec4486cbf3e -size 32319 +oid sha256:13f2b0ac39e3e5448916beea6f7a7183c8a205811343d3ff5e8a21a4d2d3f641 +size 32447 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png index fede94e2a3..f96a3743f3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9368e0cae34cde6e8559702c8de4cb42d684cea5f13cbd62d9ab0b7de918e684 -size 28624 +oid sha256:f031423cd98572a729f54e0bc4cdb50778aadd732aa93cf48d063075e9da00be +size 28636 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png index 9f40f229df..8a290a2303 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a948fedb7df6d6cd25793d9aab2a96487bd1a7bd31ba8bacb44026d0c97582c -size 36925 +oid sha256:2818a043ea06af263b1442c6e90727509da2ce2da036d68dce89de31fa8b013f +size 35614 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png index e1cff5f4ef..c1fb8f128f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e17eaf4585825f04fb121296d4b1370d652fc64d28149c285ac28849267ea9f0 -size 33051 +oid sha256:75145d74e0bebbad427626711d5bc86911bd89350e73c968c8ca9448e939cdbb +size 31954 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png index b67c6d82ed..b22bfb46dd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b0b7a756d5195a5397ef4eeba5da8b1cbdc8a4a182e935d03ef9d258d921b5f -size 33278 +oid sha256:2d5037348c8b27714e54ebec53aac8a546f14b11e3195434f681aad95e416847 +size 32181 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png index f6c53f382a..4b0a1988c5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d42ed3cb545fc96e1840db73c8efdc6782a545572539855ccceceda50663f284 -size 31445 +oid sha256:d0a34e1c3c047806e4304791664c25cd8d7c9e7850c24b2bb17119e0d83fd5d4 +size 31492 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png index 2047696c67..d0c1ab3c32 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75785d23eafb006cbfbbab3e6824364a419f56d0a7d2d2a734b6f0947ca7843b -size 25358 +oid sha256:d8c09be0dbf07d8593fd2f93881febdf3494bd439cb6f62adb78beee34b6c7e7 +size 25357 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png index 21e905b2df..05e4d69148 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:624d62ed7dd52617d8c02bcf5be4ded2707500e930d56f4362c834226e6108bd -size 32514 +oid sha256:68a54676b1f82425df6db8cb56b161950584ce9d433fc59630a16ac6288fc7d8 +size 31204 From 6ea1c408068392e7c387477de7bd5261056bea08 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 12:05:11 +0200 Subject: [PATCH 095/281] Maestro No need to close the keyboard after fix from #1593 --- .maestro/tests/roomList/searchRoomList.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index 6c31acd4db..b4f44c8b27 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -8,8 +8,6 @@ appId: ${APP_ID} # Back from timeline - back - assertVisible: "MyR" -# Close keyboard -- hideKeyboard # Back from search - back - runFlow: ../assertions/assertHomeDisplayed.yaml From 720cb681cf0bd933aba71bf25c7a1dbe61b92237 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 12:05:11 +0200 Subject: [PATCH 096/281] Maestro No need to close the keyboard after fix from #1593 --- .maestro/tests/roomList/searchRoomList.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index 6c31acd4db..b4f44c8b27 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -8,8 +8,6 @@ appId: ${APP_ID} # Back from timeline - back - assertVisible: "MyR" -# Close keyboard -- hideKeyboard # Back from search - back - runFlow: ../assertions/assertHomeDisplayed.yaml From 5cb065c3412d0ac913048acd175a9e5071c36757 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 20 Oct 2023 12:09:19 +0200 Subject: [PATCH 097/281] Revert "Maestro No need to close the keyboard after fix from #1593" This reverts commit 6ea1c408068392e7c387477de7bd5261056bea08. --- .maestro/tests/roomList/searchRoomList.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.maestro/tests/roomList/searchRoomList.yaml b/.maestro/tests/roomList/searchRoomList.yaml index b4f44c8b27..6c31acd4db 100644 --- a/.maestro/tests/roomList/searchRoomList.yaml +++ b/.maestro/tests/roomList/searchRoomList.yaml @@ -8,6 +8,8 @@ appId: ${APP_ID} # Back from timeline - back - assertVisible: "MyR" +# Close keyboard +- hideKeyboard # Back from search - back - runFlow: ../assertions/assertHomeDisplayed.yaml From f787867e87b3c84144ba2a536a12c85cba36285a Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 20 Oct 2023 14:25:20 +0200 Subject: [PATCH 098/281] Document the nuances in `UserId` and `SessionId` types. (#1616) --- .../element/android/libraries/matrix/api/core/SessionId.kt | 3 +++ .../io/element/android/libraries/matrix/api/core/UserId.kt | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt index 6009aa5c03..7bb6e27a5e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt @@ -16,4 +16,7 @@ package io.element.android.libraries.matrix.api.core +/** + * The [UserId] of the currently logged in user. + */ typealias SessionId = UserId diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index e153834501..e72af8596a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -19,6 +19,11 @@ package io.element.android.libraries.matrix.api.core import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable +/** + * A [String] holding a valid Matrix user ID. + * + * https://spec.matrix.org/v1.8/appendices/#user-identifiers + */ @JvmInline value class UserId(val value: String) : Serializable { From 5b611ed01782b918dad9cc2f9aefc4b8362d0a57 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 15:15:19 +0200 Subject: [PATCH 099/281] Lockscreen: renaming --- build.gradle.kts | 3 +- .../lockscreen/impl/LockScreenFlowNode.kt | 18 +++---- .../impl/pin/DefaultPinCodeManager.kt | 2 +- .../lockscreen/impl/pin/PinCodeManager.kt | 2 +- .../SetupPinEvents.kt} | 8 ++-- .../SetupPinNode.kt} | 8 ++-- .../SetupPinPresenter.kt} | 40 ++++++++-------- .../SetupPinState.kt} | 12 ++--- .../SetupPinStateProvider.kt} | 32 ++++++------- .../SetupPinView.kt} | 48 +++++++++---------- .../impl/{create => setup}/model/PinDigit.kt | 2 +- .../impl/{create => setup}/model/PinEntry.kt | 2 +- .../validation/CreatePinFailure.kt | 8 ++-- .../validation/PinValidator.kt | 8 ++-- .../PinUnlockEvents.kt} | 6 +-- .../PinUnlockNode.kt} | 8 ++-- .../PinUnlockPresenter.kt} | 14 +++--- .../PinUnlockState.kt} | 6 +-- .../PinUnlockStateProvider.kt} | 10 ++-- .../PinUnlockView.kt} | 25 ++++------ .../impl/{auth => unlock}/numpad/PinKeypad.kt | 2 +- .../{auth => unlock}/numpad/PinKeypadModel.kt | 2 +- .../CreatePinPresenterTest.kt | 10 ++-- 23 files changed, 135 insertions(+), 141 deletions(-) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinEvents.kt => setup/SetupPinEvents.kt} (73%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinNode.kt => setup/SetupPinNode.kt} (88%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinPresenter.kt => setup/SetupPinPresenter.kt} (73%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinState.kt => setup/SetupPinState.kt} (72%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinStateProvider.kt => setup/SetupPinStateProvider.kt} (66%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create/CreatePinView.kt => setup/SetupPinView.kt} (80%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/model/PinDigit.kt (92%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/model/PinEntry.kt (96%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/validation/CreatePinFailure.kt (74%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/validation/PinValidator.kt (80%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationEvents.kt => unlock/PinUnlockEvents.kt} (80%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationNode.kt => unlock/PinUnlockNode.kt} (86%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationPresenter.kt => unlock/PinUnlockPresenter.kt} (73%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationState.kt => unlock/PinUnlockState.kt} (80%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationStateProvider.kt => unlock/PinUnlockStateProvider.kt} (70%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth/PinAuthenticationView.kt => unlock/PinUnlockView.kt} (83%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth => unlock}/numpad/PinKeypad.kt (98%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{auth => unlock}/numpad/PinKeypadModel.kt (92%) rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/{create => setup}/CreatePinPresenterTest.kt (92%) diff --git a/build.gradle.kts b/build.gradle.kts index e14ad71981..391313fe2d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -251,8 +251,7 @@ koverMerged { // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" // Temporary until we have actually something to test. - excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter" - excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*" + excludes += "io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter$*" } bound { minValue = 85 diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index d2989d53cc..fa3b88e18a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -27,8 +27,8 @@ import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.lockscreen.impl.auth.PinAuthenticationNode -import io.element.android.features.lockscreen.impl.create.CreatePinNode +import io.element.android.features.lockscreen.impl.setup.SetupPinNode +import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -41,7 +41,7 @@ class LockScreenFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Auth, + initialElement = NavTarget.Unlock, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -50,19 +50,19 @@ class LockScreenFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - data object Auth : NavTarget + data object Unlock : NavTarget @Parcelize - data object Create : NavTarget + data object Setup : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Auth -> { - createNode(buildContext) + NavTarget.Unlock -> { + createNode(buildContext) } - NavTarget.Create -> { - createNode(buildContext) + NavTarget.Setup -> { + createNode(buildContext) } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index e7529e9280..1f7439301c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -37,7 +37,7 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } - override suspend fun createPinCode(pinCode: String) { + override suspend fun SetupPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 5f84f5296d..49b6141665 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -30,7 +30,7 @@ interface PinCodeManager { * Creates a new encrypted pin code. * @param pinCode the clear pin code to create */ - suspend fun createPinCode(pinCode: String) + suspend fun SetupPinCode(pinCode: String) /** * @return true if the pin code is correct. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt similarity index 73% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt index 78ce529325..45c5b034b0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup -sealed interface CreatePinEvents { - data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents - data object ClearFailure : CreatePinEvents +sealed interface SetupPinEvents { + data class OnPinEntryChanged(val entryAsText: String) : SetupPinEvents + data object ClearFailure : SetupPinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt similarity index 88% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt index 331d6ada84..7474289f1e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,16 +27,16 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class CreatePinNode @AssistedInject constructor( +class SetupPinNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: CreatePinPresenter, + private val presenter: SetupPinPresenter, ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { val state = presenter.present() - CreatePinView( + SetupPinView( state = state, onBackClicked = { }, modifier = modifier diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt similarity index 73% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 957594c0f7..747af1bfd4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -14,29 +14,29 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure -import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure +import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import javax.inject.Inject private const val PIN_SIZE = 4 -class CreatePinPresenter @Inject constructor( +class SetupPinPresenter @Inject constructor( private val pinValidator: PinValidator, private val buildMeta: BuildMeta, -) : Presenter { +) : Presenter { @Composable - override fun present(): CreatePinState { + override fun present(): SetupPinState { var choosePinEntry by remember { mutableStateOf(PinEntry.empty(PIN_SIZE)) } @@ -46,20 +46,20 @@ class CreatePinPresenter @Inject constructor( var isConfirmationStep by remember { mutableStateOf(false) } - var createPinFailure by remember { - mutableStateOf(null) + var setupPinFailure by remember { + mutableStateOf(null) } - fun handleEvents(event: CreatePinEvents) { + fun handleEvents(event: SetupPinEvents) { when (event) { - is CreatePinEvents.OnPinEntryChanged -> { + is SetupPinEvents.OnPinEntryChanged -> { if (isConfirmationStep) { confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) if (confirmPinEntry.isPinComplete()) { if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { - createPinFailure = CreatePinFailure.PinsDontMatch + setupPinFailure = SetupPinFailure.PinsDontMatch } } } else { @@ -67,35 +67,35 @@ class CreatePinPresenter @Inject constructor( if (choosePinEntry.isPinComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { - createPinFailure = pinValidationResult.failure + setupPinFailure = pinValidationResult.failure } PinValidator.Result.Valid -> isConfirmationStep = true } } } } - CreatePinEvents.ClearFailure -> { - when (createPinFailure) { - is CreatePinFailure.PinsDontMatch -> { + SetupPinEvents.ClearFailure -> { + when (setupPinFailure) { + is SetupPinFailure.PinsDontMatch -> { choosePinEntry = PinEntry.empty(PIN_SIZE) confirmPinEntry = PinEntry.empty(PIN_SIZE) } - is CreatePinFailure.PinBlacklisted -> { + is SetupPinFailure.PinBlacklisted -> { choosePinEntry = PinEntry.empty(PIN_SIZE) } null -> Unit } isConfirmationStep = false - createPinFailure = null + setupPinFailure = null } } } - return CreatePinState( + return SetupPinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - createPinFailure = createPinFailure, + SetupPinFailure = setupPinFailure, appName = buildMeta.applicationName, eventSink = ::handleEvents ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt similarity index 72% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt index 020076a2ab..f2b1f50e5c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure -data class CreatePinState( +data class SetupPinState( val choosePinEntry: PinEntry, val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, - val createPinFailure: CreatePinFailure?, + val SetupPinFailure: SetupPinFailure?, val appName: String, - val eventSink: (CreatePinEvents) -> Unit + val eventSink: (SetupPinEvents) -> Unit ) { val pinSize = choosePinEntry.size val activePinEntry = if (isConfirmationStep) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt similarity index 66% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt index c9dcce018d..05df16f591 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -14,47 +14,47 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure -open class CreatePinStateProvider : PreviewParameterProvider { - override val values: Sequence +open class SetupPinStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aCreatePinState(), - aCreatePinState( + aSetupPinState(), + aSetupPinState( choosePinEntry = PinEntry.empty(4).fillWith("12") ), - aCreatePinState( + aSetupPinState( choosePinEntry = PinEntry.empty(4).fillWith("1789"), isConfirmationStep = true, ), - aCreatePinState( + aSetupPinState( choosePinEntry = PinEntry.empty(4).fillWith("1789"), confirmPinEntry = PinEntry.empty(4).fillWith("1788"), isConfirmationStep = true, - creationFailure = CreatePinFailure.PinsDontMatch + creationFailure = SetupPinFailure.PinsDontMatch ), - aCreatePinState( + aSetupPinState( choosePinEntry = PinEntry.empty(4).fillWith("1111"), - creationFailure = CreatePinFailure.PinBlacklisted + creationFailure = SetupPinFailure.PinBlacklisted ), ) } -fun aCreatePinState( +fun aSetupPinState( choosePinEntry: PinEntry = PinEntry.empty(4), confirmPinEntry: PinEntry = PinEntry.empty(4), isConfirmationStep: Boolean = false, - creationFailure: CreatePinFailure? = null, -) = CreatePinState( + creationFailure: SetupPinFailure? = null, +) = SetupPinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - createPinFailure = creationFailure, + SetupPinFailure = creationFailure, appName = "Element", eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index 063d65f41f..3b84e1d889 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -46,9 +46,9 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R -import io.element.android.features.lockscreen.impl.create.model.PinDigit -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.setup.model.PinDigit +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog @@ -61,8 +61,8 @@ import io.element.android.libraries.designsystem.theme.pinDigitBg import io.element.android.libraries.theme.ElementTheme @Composable -fun CreatePinView( - state: CreatePinState, +fun SetupPinView( + state: SetupPinState, onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { @@ -86,15 +86,15 @@ fun CreatePinView( .verticalScroll(state = scrollState) .padding(vertical = 16.dp, horizontal = 20.dp), ) { - CreatePinHeader(state.isConfirmationStep, state.appName) - CreatePinContent(state) + SetupPinHeader(state.isConfirmationStep, state.appName) + SetupPinContent(state) } } ) } @Composable -private fun CreatePinHeader( +private fun SetupPinHeader( isValidationStep: Boolean, appName: String, modifier: Modifier = Modifier, @@ -116,44 +116,44 @@ private fun CreatePinHeader( } @Composable -private fun CreatePinContent( - state: CreatePinState, +private fun SetupPinContent( + state: SetupPinState, modifier: Modifier = Modifier, ) { PinEntryTextField( state.activePinEntry, onValueChange = { - state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(it)) }, modifier = modifier .padding(top = 36.dp) .fillMaxWidth() ) - if (state.createPinFailure != null) { + if (state.SetupPinFailure != null) { ErrorDialog( modifier = modifier, - title = state.createPinFailure.title(), - content = state.createPinFailure.content(), + title = state.SetupPinFailure.title(), + content = state.SetupPinFailure.content(), onDismiss = { - state.eventSink(CreatePinEvents.ClearFailure) + state.eventSink(SetupPinEvents.ClearFailure) } ) } } @Composable -private fun CreatePinFailure.content(): String { +private fun SetupPinFailure.content(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_content) - CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content) + SetupPinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_content) + SetupPinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content) } } @Composable -private fun CreatePinFailure.title(): String { +private fun SetupPinFailure.title(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_title) - CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title) + SetupPinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_title) + SetupPinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title) } } @@ -225,9 +225,9 @@ private fun PinDigitView( @Composable @PreviewsDayNight -internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { +internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStateProvider::class) state: SetupPinState) { ElementPreview { - CreatePinView( + SetupPinView( state = state, onBackClicked = {}, ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt similarity index 92% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt index 741a61cafe..10a4832866 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.model +package io.element.android.features.lockscreen.impl.setup.model sealed interface PinDigit { data object Empty : PinDigit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt similarity index 96% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt index a97315f2e8..9f802a989c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.model +package io.element.android.features.lockscreen.impl.setup.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt similarity index 74% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt index 8c0cb78921..3bb21cb9e6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.validation +package io.element.android.features.lockscreen.impl.setup.validation -sealed interface CreatePinFailure { - data object PinBlacklisted : CreatePinFailure - data object PinsDontMatch : CreatePinFailure +sealed interface SetupPinFailure { + data object PinBlacklisted : SetupPinFailure + data object PinsDontMatch : SetupPinFailure } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index 7353ec47d0..0ddf86d887 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.validation +package io.element.android.features.lockscreen.impl.setup.validation import androidx.annotation.VisibleForTesting -import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.model.PinEntry import javax.inject.Inject class PinValidator @Inject constructor() { @@ -29,14 +29,14 @@ class PinValidator @Inject constructor() { sealed interface Result { data object Valid : Result - data class Invalid(val failure: CreatePinFailure) : Result + data class Invalid(val failure: SetupPinFailure) : Result } fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() val isBlacklisted = BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { - Result.Invalid(CreatePinFailure.PinBlacklisted) + Result.Invalid(SetupPinFailure.PinBlacklisted) } else { Result.Valid } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index f9f46c430a..ba35f3045c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -14,8 +14,8 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock -sealed interface PinAuthenticationEvents { - data object Unlock : PinAuthenticationEvents +sealed interface PinUnlockEvents { + data object Unlock : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt similarity index 86% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt index d236d40cf1..0fba55c17b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,16 +27,16 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class PinAuthenticationNode @AssistedInject constructor( +class PinUnlockNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: PinAuthenticationPresenter, + private val presenter: PinUnlockPresenter, ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { val state = presenter.present() - PinAuthenticationView( + PinUnlockView( state = state, modifier = modifier ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt similarity index 73% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index ecc82f421c..24cba574af 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable import io.element.android.features.lockscreen.api.LockScreenStateService @@ -23,20 +23,20 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject -class PinAuthenticationPresenter @Inject constructor( +class PinUnlockPresenter @Inject constructor( private val pinStateService: LockScreenStateService, private val coroutineScope: CoroutineScope, -) : Presenter { +) : Presenter { @Composable - override fun present(): PinAuthenticationState { + override fun present(): PinUnlockState { - fun handleEvents(event: PinAuthenticationEvents) { + fun handleEvents(event: PinUnlockEvents) { when (event) { - PinAuthenticationEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } + PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } } } - return PinAuthenticationState( + return PinUnlockState( eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt similarity index 80% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 387467534f..dec731c040 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -14,8 +14,8 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock -data class PinAuthenticationState( - val eventSink: (PinAuthenticationEvents) -> Unit +data class PinUnlockState( + val eventSink: (PinUnlockEvents) -> Unit ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt similarity index 70% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index a2612ed858..f5afb44fa9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -14,17 +14,17 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class PinAuthenticationStateProvider : PreviewParameterProvider { - override val values: Sequence +open class PinUnlockStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aPinAuthenticationState(), + aPinUnlockState(), ) } -fun aPinAuthenticationState() = PinAuthenticationState( +fun aPinUnlockState() = PinUnlockState( eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt similarity index 83% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 740d85adec..47027853b4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -14,23 +14,18 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock @@ -41,7 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.lockscreen.impl.auth.numpad.PinKeypad +import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -52,13 +47,13 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @Composable -fun PinAuthenticationView( - state: PinAuthenticationState, +fun PinUnlockView( + state: PinUnlockState, modifier: Modifier = Modifier, ) { Surface(modifier) { HeaderFooterPage( - header = { PinAuthenticationHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, + header = { PinUnlockHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, content = { Box( modifier = Modifier @@ -76,12 +71,12 @@ fun PinAuthenticationView( } @Composable -private fun PinAuthenticationFooter(state: PinAuthenticationState) { +private fun PinUnlockFooter(state: PinUnlockState) { Button( modifier = Modifier.fillMaxWidth(), text = "Unlock", onClick = { - state.eventSink(PinAuthenticationEvents.Unlock) + state.eventSink(PinUnlockEvents.Unlock) } ) } @@ -116,7 +111,7 @@ private fun PinDot( } @Composable -private fun PinAuthenticationHeader( +private fun PinUnlockHeader( modifier: Modifier = Modifier, ) { Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { @@ -151,9 +146,9 @@ private fun PinAuthenticationHeader( @Composable @PreviewsDayNight -internal fun PinAuthenticationViewPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) { +internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) { ElementPreview { - PinAuthenticationView( + PinUnlockView( state = state, ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt similarity index 98% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index 43abc24002..44a09ed08f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth.numpad +package io.element.android.features.lockscreen.impl.unlock.numpad import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt similarity index 92% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt index 108486e400..4ea42e7f42 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/numpad/PinKeypadModel.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth.numpad +package io.element.android.features.lockscreen.impl.unlock.numpad import androidx.compose.runtime.Immutable diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt similarity index 92% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt index 78536bb693..85f54fd149 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt @@ -14,16 +14,16 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.create.model.PinDigit -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure -import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.model.PinDigit +import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest From 1d3a75fbd42554e7cd58d8f7b7f1348230e335e6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 15:23:10 +0200 Subject: [PATCH 100/281] Pin : move some classes around --- .../impl/components/PinEntryTextField.kt | 116 ++++++++++++++++++ .../impl/{setup => pin}/model/PinDigit.kt | 2 +- .../impl/{setup => pin}/model/PinEntry.kt | 2 +- .../impl/setup/SetupPinPresenter.kt | 2 +- .../lockscreen/impl/setup/SetupPinState.kt | 2 +- .../impl/setup/SetupPinStateProvider.kt | 2 +- .../lockscreen/impl/setup/SetupPinView.kt | 83 +------------ .../impl/setup/validation/PinValidator.kt | 2 +- .../impl/setup/CreatePinPresenterTest.kt | 4 +- 9 files changed, 125 insertions(+), 90 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{setup => pin}/model/PinDigit.kt (93%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{setup => pin}/model/PinEntry.kt (96%) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt new file mode 100644 index 0000000000..5682e594fe --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -0,0 +1,116 @@ +/* + * 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.lockscreen.impl.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.pin.model.PinDigit +import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.pinDigitBg +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun PinEntryTextField( + pinEntry: PinEntry, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + BasicTextField( + modifier = modifier, + value = TextFieldValue(pinEntry.toText()), + onValueChange = { + onValueChange(it.text) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + decorationBox = { + PinEntryRow(pinEntry = pinEntry) + } + ) +} + +@Composable +private fun PinEntryRow( + pinEntry: PinEntry, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + for (digit in pinEntry.digits) { + PinDigitView(digit = digit) + } + } +} + +@Composable +private fun PinDigitView( + digit: PinDigit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(8.dp) + val appearanceModifier = when (digit) { + PinDigit.Empty -> { + Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) + } + is PinDigit.Filled -> { + Modifier.background(ElementTheme.colors.pinDigitBg, shape) + } + } + Box( + modifier = modifier + .size(48.dp) + .then(appearanceModifier), + contentAlignment = Alignment.Center, + + ) { + if (digit is PinDigit.Filled) { + Text( + text = digit.toText(), + style = ElementTheme.typography.fontHeadingMdBold + ) + } + + } +} + +@PreviewsDayNight +@Composable +fun PinEntryTextFieldPreview() { + ElementTheme { + PinEntryTextField( + pinEntry = PinEntry.empty(4).fillWith("12"), + onValueChange = {}, + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt similarity index 93% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt index 10a4832866..aa3c45e02e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinDigit.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.setup.model +package io.element.android.features.lockscreen.impl.pin.model sealed interface PinDigit { data object Empty : PinDigit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt similarity index 96% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 9f802a989c..d14b71ffe2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.setup.model +package io.element.android.features.lockscreen.impl.pin.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 747af1bfd4..af34c387d2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.libraries.architecture.Presenter diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt index f2b1f50e5c..7823b1e39f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt @@ -16,7 +16,7 @@ package io.element.android.features.lockscreen.impl.setup -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure data class SetupPinState( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt index 05df16f591..37a232dc11 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.lockscreen.impl.setup import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure open class SetupPinStateProvider : PreviewParameterProvider { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index 3b84e1d889..18be5e14b4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -18,21 +18,12 @@ package io.element.android.features.lockscreen.impl.setup -import androidx.compose.foundation.background -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.consumeWindowInsets 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.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock @@ -41,13 +32,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R -import io.element.android.features.lockscreen.impl.setup.model.PinDigit -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.components.PinEntryTextField import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton @@ -55,10 +43,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog 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 -import io.element.android.libraries.designsystem.theme.pinDigitBg -import io.element.android.libraries.theme.ElementTheme @Composable fun SetupPinView( @@ -157,72 +142,6 @@ private fun SetupPinFailure.title(): String { } } -@Composable -private fun PinEntryTextField( - pinEntry: PinEntry, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, -) { - BasicTextField( - modifier = modifier, - value = TextFieldValue(pinEntry.toText()), - onValueChange = { - onValueChange(it.text) - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - decorationBox = { - PinEntryRow(pinEntry = pinEntry) - } - ) -} - -@Composable -private fun PinEntryRow( - pinEntry: PinEntry, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - ) { - for (digit in pinEntry.digits) { - PinDigitView(digit = digit) - } - } -} - -@Composable -private fun PinDigitView( - digit: PinDigit, - modifier: Modifier = Modifier, -) { - val shape = RoundedCornerShape(8.dp) - val appearanceModifier = when (digit) { - PinDigit.Empty -> { - Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) - } - is PinDigit.Filled -> { - Modifier.background(ElementTheme.colors.pinDigitBg, shape) - } - } - Box( - modifier = modifier - .size(48.dp) - .then(appearanceModifier), - contentAlignment = Alignment.Center, - - ) { - if (digit is PinDigit.Filled) { - Text( - text = digit.toText(), - style = ElementTheme.typography.fontHeadingMdBold - ) - } - - } -} - @Composable @PreviewsDayNight internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStateProvider::class) state: SetupPinState) { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index 0ddf86d887..c7435120aa 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -17,7 +17,7 @@ package io.element.android.features.lockscreen.impl.setup.validation import androidx.annotation.VisibleForTesting -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject class PinValidator @Inject constructor() { diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt index 85f54fd149..723b8e8e6b 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt @@ -20,8 +20,8 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.setup.model.PinDigit -import io.element.android.features.lockscreen.impl.setup.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.PinDigit +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.libraries.matrix.test.core.aBuildMeta From 55a272d8b7eb03d8e7f651e57f94b00be33f02de Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 15:43:55 +0200 Subject: [PATCH 101/281] Pin unlock : start branching logic --- .../lockscreen/impl/pin/model/PinEntry.kt | 24 ++++++++++++- .../impl/setup/SetupPinPresenter.kt | 4 +-- .../state/DefaultLockScreenStateService.kt | 3 +- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 3 ++ .../impl/unlock/PinUnlockPresenter.kt | 25 +++++++++++++ .../lockscreen/impl/unlock/PinUnlockState.kt | 3 ++ .../impl/unlock/PinUnlockStateProvider.kt | 6 +++- .../lockscreen/impl/unlock/PinUnlockView.kt | 36 +++++++++---------- .../impl/unlock/numpad/PinKeypad.kt | 10 +++--- .../impl/unlock/numpad/PinKeypadModel.kt | 2 +- 10 files changed, 85 insertions(+), 31 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index d14b71ffe2..92dda869a6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -50,14 +50,36 @@ data class PinEntry( return copy(digits = newDigits.toPersistentList()) } + fun deleteLast(): PinEntry { + if (isEmpty()) return this + val newDigits = digits.toMutableList() + newDigits.indexOfLast { it is PinDigit.Filled }.also { lastFilled -> + newDigits[lastFilled] = PinDigit.Empty + } + return copy(digits = newDigits.toPersistentList()) + } + + fun addDigit(digit: Char): PinEntry { + if (isComplete()) return this + val newDigits = digits.toMutableList() + newDigits.indexOfFirst { it is PinDigit.Empty }.also { firstEmpty -> + newDigits[firstEmpty] = PinDigit.Filled(digit) + } + return copy(digits = newDigits.toPersistentList()) + } + fun clear(): PinEntry { return fillWith("") } - fun isPinComplete(): Boolean { + fun isComplete(): Boolean { return digits.all { it is PinDigit.Filled } } + fun isEmpty(): Boolean { + return digits.all { it is PinDigit.Empty } + } + fun toText(): String { return digits.joinToString("") { it.toText() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index af34c387d2..8561e5333c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -55,7 +55,7 @@ class SetupPinPresenter @Inject constructor( is SetupPinEvents.OnPinEntryChanged -> { if (isConfirmationStep) { confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) - if (confirmPinEntry.isPinComplete()) { + if (confirmPinEntry.isComplete()) { if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { @@ -64,7 +64,7 @@ class SetupPinPresenter @Inject constructor( } } else { choosePinEntry = choosePinEntry.fillWith(event.entryAsText) - if (choosePinEntry.isPinComplete()) { + if (choosePinEntry.isComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { setupPinFailure = pinValidationResult.failure diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt index dbfeca2c6a..a071de06e9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt @@ -25,7 +25,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -57,7 +56,7 @@ class DefaultLockScreenStateService @Inject constructor( override suspend fun entersBackground() = coroutineScope { lockJob = launch { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - delay(GRACE_PERIOD_IN_MILLIS) + //delay(GRACE_PERIOD_IN_MILLIS) _lockScreenState.value = LockScreenState.Locked } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index ba35f3045c..8dddd40e8a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -16,6 +16,9 @@ package io.element.android.features.lockscreen.impl.unlock +import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel + sealed interface PinUnlockEvents { + data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents data object Unlock : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 24cba574af..d673ad7966 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -17,7 +17,13 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.api.LockScreenStateService +import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -31,13 +37,32 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { + var pinEntry by remember { + mutableStateOf(PinEntry.empty(4)) + } + fun handleEvents(event: PinUnlockEvents) { when (event) { PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } + is PinUnlockEvents.OnPinKeypadPressed -> { + pinEntry = pinEntry.process(event.pinKeypadModel) + if (pinEntry.isComplete()) { + coroutineScope.launch { pinStateService.unlock() } + } + } } } return PinUnlockState( + pinEntry = pinEntry, eventSink = ::handleEvents ) } + + private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry { + return when (pinKeypadModel) { + PinKeypadModel.Back -> deleteLast() + is PinKeypadModel.Number -> addDigit(pinKeypadModel.number) + PinKeypadModel.Empty -> this + } + } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index dec731c040..69d3213c7e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -16,6 +16,9 @@ package io.element.android.features.lockscreen.impl.unlock +import io.element.android.features.lockscreen.impl.pin.model.PinEntry + data class PinUnlockState( + val pinEntry: PinEntry, val eventSink: (PinUnlockEvents) -> Unit ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index f5afb44fa9..5120818316 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.lockscreen.impl.pin.model.PinEntry open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence @@ -25,6 +26,9 @@ open class PinUnlockStateProvider : PreviewParameterProvider { ) } -fun aPinUnlockState() = PinUnlockState( +fun aPinUnlockState( + pinEntry: PinEntry = PinEntry.empty(4), +) = PinUnlockState( + pinEntry = pinEntry, eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 47027853b4..5631f33e32 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -36,11 +36,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.pin.model.PinDigit +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage 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.Surface import io.element.android.libraries.designsystem.theme.components.Text @@ -53,7 +54,12 @@ fun PinUnlockView( ) { Surface(modifier) { HeaderFooterPage( - header = { PinUnlockHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, + header = { + PinUnlockHeader( + state = state, + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp) + ) + }, content = { Box( modifier = Modifier @@ -62,7 +68,9 @@ fun PinUnlockView( contentAlignment = Alignment.Center, ) { PinKeypad( - onClick = {} + onClick = { + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) + } ) } } @@ -70,26 +78,15 @@ fun PinUnlockView( } } -@Composable -private fun PinUnlockFooter(state: PinUnlockState) { - Button( - modifier = Modifier.fillMaxWidth(), - text = "Unlock", - onClick = { - state.eventSink(PinUnlockEvents.Unlock) - } - ) -} - @Composable private fun PinDotsRow( + pinEntry: PinEntry, modifier: Modifier = Modifier, ) { Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { - PinDot(isFilled = true) - PinDot(isFilled = true) - PinDot(isFilled = false) - PinDot(isFilled = false) + for (digit in pinEntry.digits) { + PinDot(isFilled = digit is PinDigit.Filled) + } } } @@ -112,6 +109,7 @@ private fun PinDot( @Composable private fun PinUnlockHeader( + state: PinUnlockState, modifier: Modifier = Modifier, ) { Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { @@ -140,7 +138,7 @@ private fun PinUnlockHeader( color = MaterialTheme.colorScheme.secondary, ) Spacer(Modifier.height(24.dp)) - PinDotsRow() + PinDotsRow(state.pinEntry) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index 44a09ed08f..d22a34b732 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -61,25 +61,25 @@ fun PinKeypad( PinKeypadRow( verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number("1"), PinKeypadModel.Number("2"), PinKeypadModel.Number("3")), + models = listOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), onClick = onClick, ) PinKeypadRow( verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number("4"), PinKeypadModel.Number("5"), PinKeypadModel.Number("6")), + models = listOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), onClick = onClick, ) PinKeypadRow( verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number("7"), PinKeypadModel.Number("8"), PinKeypadModel.Number("9")), + models = listOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), onClick = onClick, ) PinKeypadRow( verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number("0"), PinKeypadModel.Back), + models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), onClick = onClick, ) } @@ -114,7 +114,7 @@ private fun PinKeypadRow( PinKeyBadDigitButton( size = 80.dp, modifier = commonModifier, - digit = model.number, + digit = model.number.toString(), onClick = { onClick(model) }, ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt index 4ea42e7f42..f1430dcaa5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt @@ -22,5 +22,5 @@ import androidx.compose.runtime.Immutable sealed interface PinKeypadModel { data object Empty : PinKeypadModel data object Back : PinKeypadModel - data class Number(val number: String) : PinKeypadModel + data class Number(val number: Char) : PinKeypadModel } From a6060ab4ab345a29e170caf60be14e603561ac68 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 16:35:54 +0200 Subject: [PATCH 102/281] Pin unlock : fix some ui --- .../lockscreen/impl/unlock/PinUnlockView.kt | 8 +++- .../impl/unlock/numpad/PinKeypad.kt | 40 ++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 5631f33e32..686790afcb 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -65,12 +66,15 @@ fun PinUnlockView( modifier = Modifier .padding(top = 40.dp) .fillMaxWidth(), - contentAlignment = Alignment.Center, ) { PinKeypad( onClick = { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) - } + }, + horizontalArrangement = spacedBy(24.dp, Alignment.CenterHorizontally), + verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index d22a34b732..39656a8d56 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -17,18 +17,21 @@ package io.element.android.features.lockscreen.impl.unlock.numpad import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Backspace import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -96,7 +99,7 @@ private fun PinKeypadRow( Row( horizontalArrangement = horizontalArrangement, verticalAlignment = verticalAlignment, - modifier = modifier, + modifier = modifier.fillMaxWidth(), ) { val commonModifier = Modifier.size(80.dp) for (model in models) { @@ -123,6 +126,22 @@ private fun PinKeypadRow( } } +@Composable +private fun PinKeypadButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(CircleShape) + .background(color = ElementTheme.colors.bgSubtlePrimary) + .clickable(onClick = onClick), + content = content + ) +} + @Composable private fun PinKeyBadDigitButton( digit: String, @@ -130,15 +149,8 @@ private fun PinKeyBadDigitButton( onClick: (String) -> Unit, modifier: Modifier = Modifier, ) { - Button( - colors = ButtonDefaults.buttonColors( - containerColor = ElementTheme.colors.bgSubtlePrimary, - contentColor = Color.Transparent, - ), - shape = CircleShape, - contentPadding = PaddingValues(0.dp), - modifier = modifier - .clip(CircleShape), + PinKeypadButton( + modifier = modifier, onClick = { onClick(digit) } ) { val fontSize = 80.dp.toSp() / 2 @@ -158,10 +170,8 @@ private fun PinKeypadBackButton( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - IconButton( - modifier = modifier - .clip(CircleShape) - .background(color = Color.Transparent, shape = CircleShape), + PinKeypadButton( + modifier = modifier, onClick = onClick, ) { Icon( From f12de0ce1d57c650f4f526897639b8e6af1f5db5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 17:57:29 +0200 Subject: [PATCH 103/281] Pin unlock : best effort for small height --- .../lockscreen/impl/unlock/PinUnlockView.kt | 122 +++++++++++++++--- 1 file changed, 101 insertions(+), 21 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 686790afcb..4f4f9be2df 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -17,16 +17,20 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight 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.size +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock @@ -34,18 +38,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad -import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage 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 import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.theme.ElementTheme @Composable @@ -54,31 +60,95 @@ fun PinUnlockView( modifier: Modifier = Modifier, ) { Surface(modifier) { - HeaderFooterPage( - header = { + BoxWithConstraints { + val commonModifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(all = 20.dp) + + val header = @Composable { PinUnlockHeader( state = state, modifier = Modifier.padding(top = 60.dp, bottom = 12.dp) ) - }, - content = { - Box( - modifier = Modifier - .padding(top = 40.dp) - .fillMaxWidth(), - ) { - PinKeypad( - onClick = { - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) - }, - horizontalArrangement = spacedBy(24.dp, Alignment.CenterHorizontally), - verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), - horizontalAlignment = Alignment.CenterHorizontally, - verticalAlignment = Alignment.CenterVertically, - ) - } } - ) + val footer = @Composable { + PinUnlockFooter() + } + val content = @Composable { + PinKeypad( + onClick = { + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) + }, + horizontalArrangement = spacedBy(24.dp, Alignment.CenterHorizontally), + verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) + } + if (maxHeight < 600.dp) { + PinUnlockCompactView( + header = header, + footer = footer, + content = content, + modifier = commonModifier, + ) + } else { + PinUnlockExpandedView( + header = header, + footer = footer, + content = content, + modifier = commonModifier, + ) + } + } + } +} + +@Composable +fun PinUnlockCompactView( + modifier: Modifier = Modifier, + header: @Composable () -> Unit, + footer: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + Row(modifier = modifier) { + Column(Modifier.weight(1f)) { + header() + Spacer(modifier = Modifier.height(24.dp)) + footer() + } + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + contentAlignment = Alignment.Center, + ) { + content() + } + } +} + +@Composable +fun PinUnlockExpandedView( + modifier: Modifier = Modifier, + header: @Composable () -> Unit, + footer: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier, + ) { + header() + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(top = 40.dp), + ) { + content() + } + footer() } } @@ -146,6 +216,16 @@ private fun PinUnlockHeader( } } +@Composable +private fun PinUnlockFooter( + modifier: Modifier = Modifier, +) { + Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { + TextButton(text = "Use biometric", onClick = { }) + TextButton(text = stringResource(id = R.string.screen_app_lock_forgot_pin), onClick = { }) + } +} + @Composable @PreviewsDayNight internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) { From 67d4271f4d81e701517de1b61ac4399d88c571de Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 18:52:56 +0200 Subject: [PATCH 104/281] Pin unlock : add signout prompt --- features/lockscreen/impl/build.gradle.kts | 1 + .../impl/pin/DefaultPinCodeManager.kt | 2 +- .../lockscreen/impl/pin/PinCodeManager.kt | 2 +- .../lockscreen/impl/pin/model/PinEntry.kt | 3 +- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 1 + .../impl/unlock/PinUnlockPresenter.kt | 19 ++++++++-- .../lockscreen/impl/unlock/PinUnlockState.kt | 7 +++- .../impl/unlock/PinUnlockStateProvider.kt | 10 ++++++ .../lockscreen/impl/unlock/PinUnlockView.kt | 36 +++++++++++++++++-- .../impl/PreferencesFeatureFlagProvider.kt | 5 +-- 10 files changed, 74 insertions(+), 12 deletions(-) diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 028d8bee3c..8daeb2178c 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.cryptography.api) + implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index 1f7439301c..f5848a9d40 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -37,7 +37,7 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } - override suspend fun SetupPinCode(pinCode: String) { + override suspend fun setupPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 49b6141665..09197c3eb1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -30,7 +30,7 @@ interface PinCodeManager { * Creates a new encrypted pin code. * @param pinCode the clear pin code to create */ - suspend fun SetupPinCode(pinCode: String) + suspend fun setupPinCode(pinCode: String) /** * @return true if the pin code is correct. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 92dda869a6..76331bd38f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -18,10 +18,11 @@ package io.element.android.features.lockscreen.impl.pin.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList +import java.io.Serializable data class PinEntry( val digits: ImmutableList, -) { +): Serializable { companion object { fun empty(size: Int): PinEntry { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index 8dddd40e8a..a90b6cb702 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -21,4 +21,5 @@ import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel sealed interface PinUnlockEvents { data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents data object Unlock : PinUnlockEvents + data object OnForgetPin : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index d673ad7966..d6b77f799c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -18,8 +18,9 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.features.lockscreen.impl.pin.model.PinEntry @@ -36,10 +37,18 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { - - var pinEntry by remember { + var pinEntry by rememberSaveable { mutableStateOf(PinEntry.empty(4)) } + var remainingAttempts by rememberSaveable { + mutableIntStateOf(3) + } + var showWrongPinTitle by rememberSaveable { + mutableStateOf(false) + } + var showSignOutPrompt by rememberSaveable { + mutableStateOf(false) + } fun handleEvents(event: PinUnlockEvents) { when (event) { @@ -50,10 +59,14 @@ class PinUnlockPresenter @Inject constructor( coroutineScope.launch { pinStateService.unlock() } } } + PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true } } return PinUnlockState( pinEntry = pinEntry, + showWrongPinTitle = showWrongPinTitle, + remainingAttempts = remainingAttempts, + showSignOutPrompt = showSignOutPrompt, eventSink = ::handleEvents ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 69d3213c7e..1787fb8e8b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -20,5 +20,10 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry data class PinUnlockState( val pinEntry: PinEntry, + val showWrongPinTitle: Boolean, + val remainingAttempts: Int, + val showSignOutPrompt: Boolean, val eventSink: (PinUnlockEvents) -> Unit -) +) { + val isSignOutPromptCancellable = remainingAttempts > 0 +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 5120818316..4f269d2f5a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -23,12 +23,22 @@ open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPinUnlockState(), + aPinUnlockState(pinEntry = PinEntry.empty(4).fillWith("12")), + aPinUnlockState(showWrongPinTitle = true), + aPinUnlockState(showSignOutPrompt = true), + aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), ) } fun aPinUnlockState( pinEntry: PinEntry = PinEntry.empty(4), + remainingAttempts: Int = 3, + showWrongPinTitle: Boolean = false, + showSignOutPrompt: Boolean = false, ) = PinUnlockState( pinEntry = pinEntry, + showWrongPinTitle = showWrongPinTitle, + remainingAttempts = remainingAttempts, + showSignOutPrompt = showSignOutPrompt, eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 4f4f9be2df..19cb866a5c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter @@ -46,6 +47,8 @@ import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog 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 @@ -53,6 +56,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings @Composable fun PinUnlockView( @@ -101,6 +105,22 @@ fun PinUnlockView( modifier = commonModifier, ) } + if (state.showSignOutPrompt) { + if (state.isSignOutPromptCancellable) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_app_lock_signout_alert_title), + content = stringResource(id = R.string.screen_app_lock_signout_alert_message), + onSubmitClicked = {}, + onDismiss = {}, + ) + } else { + ErrorDialog( + title = stringResource(id = R.string.screen_app_lock_signout_alert_title), + content = stringResource(id = R.string.screen_app_lock_signout_alert_message), + onDismiss = {}, + ) + } + } } } } @@ -196,7 +216,7 @@ private fun PinUnlockHeader( ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Enter your PIN", + text = stringResource(id = CommonStrings.common_enter_your_pin), modifier = Modifier .fillMaxWidth(), textAlign = TextAlign.Center, @@ -204,12 +224,22 @@ private fun PinUnlockHeader( color = MaterialTheme.colorScheme.primary, ) Spacer(Modifier.height(8.dp)) + val subtitle = if (state.showWrongPinTitle) { + pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = state.remainingAttempts, state.remainingAttempts) + } else { + stringResource(id = R.string.screen_app_lock_subtitle) + } + val subtitleColor = if (state.showWrongPinTitle) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.secondary + } Text( - text = "You have 3 attempts to unlock", + text = subtitle, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.secondary, + color = subtitleColor, ) Spacer(Modifier.height(24.dp)) PinDotsRow(state.pinEntry) diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index ddffdebd34..23ff977da2 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -24,6 +24,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.featureflag.api.Feature +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -44,10 +45,10 @@ class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext con } } - override suspend fun isFeatureEnabled(feature: Feature): Boolean { + override fun isFeatureEnabled(feature: Feature): Flow { return store.data.map { prefs -> prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue - }.first() + } } override fun hasFeature(feature: Feature): Boolean { From 4a24e1cd7e2fc2988eee9a7e22ff288edef8385b Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 20:25:54 +0200 Subject: [PATCH 105/281] Pin unlock : better PinKeypad management --- .../lockscreen/impl/unlock/PinUnlockView.kt | 16 +++---- .../impl/unlock/numpad/PinKeypad.kt | 42 ++++++++++++++----- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 19cb866a5c..2fff711131 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -79,15 +80,14 @@ fun PinUnlockView( val footer = @Composable { PinUnlockFooter() } - val content = @Composable { + val content = @Composable { constraints: BoxWithConstraintsScope -> PinKeypad( onClick = { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) }, - horizontalArrangement = spacedBy(24.dp, Alignment.CenterHorizontally), - verticalArrangement = spacedBy(16.dp, Alignment.CenterVertically), + maxWidth = constraints.maxWidth, + maxHeight = constraints.maxHeight, horizontalAlignment = Alignment.CenterHorizontally, - verticalAlignment = Alignment.CenterVertically, ) } if (maxHeight < 600.dp) { @@ -130,7 +130,7 @@ fun PinUnlockCompactView( modifier: Modifier = Modifier, header: @Composable () -> Unit, footer: @Composable () -> Unit, - content: @Composable () -> Unit, + content: @Composable BoxWithConstraintsScope.() -> Unit, ) { Row(modifier = modifier) { Column(Modifier.weight(1f)) { @@ -138,7 +138,7 @@ fun PinUnlockCompactView( Spacer(modifier = Modifier.height(24.dp)) footer() } - Box( + BoxWithConstraints( modifier = Modifier .weight(1f) .fillMaxHeight(), @@ -154,13 +154,13 @@ fun PinUnlockExpandedView( modifier: Modifier = Modifier, header: @Composable () -> Unit, footer: @Composable () -> Unit, - content: @Composable () -> Unit, + content: @Composable BoxWithConstraintsScope.() -> Unit, ) { Column( modifier = modifier, ) { header() - Box( + BoxWithConstraints( modifier = Modifier .weight(1f) .fillMaxWidth() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index 39656a8d56..ee8adb19b2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -19,10 +19,11 @@ package io.element.android.features.lockscreen.impl.unlock.numpad import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -30,56 +31,68 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Backspace -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.coerceAtMost import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.times import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toSp import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.theme.ElementTheme +private val spaceBetweenPinKey = 8.dp +private val maxSizePinKey = 80.dp + @Composable fun PinKeypad( onClick: (PinKeypadModel) -> Unit, + maxWidth: Dp, + maxHeight: Dp, modifier: Modifier = Modifier, - verticalArrangement: Arrangement.Vertical = Arrangement.Top, verticalAlignment: Alignment.Vertical = Alignment.Top, - horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, horizontalAlignment: Alignment.Horizontal = Alignment.Start, ) { + val pinKeyMaxWidth = ((maxWidth - 2 * spaceBetweenPinKey) / 3).coerceAtMost(maxSizePinKey) + val pinKeyMaxHeight = ((maxHeight - 3 * spaceBetweenPinKey) / 4).coerceAtMost(maxSizePinKey) + val pinKeySize = if (pinKeyMaxWidth < pinKeyMaxHeight) pinKeyMaxWidth else pinKeyMaxHeight + + val horizontalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterHorizontally) + val verticalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterVertically) Column( modifier = modifier, verticalArrangement = verticalArrangement, horizontalAlignment = horizontalAlignment, ) { PinKeypadRow( + pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, models = listOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), onClick = onClick, ) PinKeypadRow( + pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, models = listOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), onClick = onClick, ) PinKeypadRow( + pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, models = listOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), onClick = onClick, ) PinKeypadRow( + pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), @@ -92,6 +105,7 @@ fun PinKeypad( private fun PinKeypadRow( models: List, onClick: (PinKeypadModel) -> Unit, + pinKeySize: Dp, modifier: Modifier = Modifier, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, @@ -101,7 +115,7 @@ private fun PinKeypadRow( verticalAlignment = verticalAlignment, modifier = modifier.fillMaxWidth(), ) { - val commonModifier = Modifier.size(80.dp) + val commonModifier = Modifier.size(pinKeySize) for (model in models) { when (model) { is PinKeypadModel.Empty -> { @@ -115,7 +129,7 @@ private fun PinKeypadRow( } is PinKeypadModel.Number -> { PinKeyBadDigitButton( - size = 80.dp, + size = pinKeySize, modifier = commonModifier, digit = model.number.toString(), onClick = { onClick(model) }, @@ -153,7 +167,7 @@ private fun PinKeyBadDigitButton( modifier = modifier, onClick = { onClick(digit) } ) { - val fontSize = 80.dp.toSp() / 2 + val fontSize = size.toSp() / 2 val originalFont = ElementTheme.typography.fontHeadingXlBold val ratio = fontSize.value / originalFont.fontSize.value val lineHeight = originalFont.lineHeight * ratio @@ -183,9 +197,15 @@ private fun PinKeypadBackButton( @Composable @PreviewsDayNight -fun PinKeypad() { +fun PinKeypadPreview() { ElementPreview { - PinKeypad(onClick = {}) + BoxWithConstraints { + PinKeypad( + maxWidth = maxWidth, + maxHeight = maxHeight, + onClick = {} + ) + } } } From fd63220ee08abdd73da1578d87da5ed0d5d68259 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 20:26:04 +0200 Subject: [PATCH 106/281] Fix compilation --- .../android/features/lockscreen/impl/pin/model/PinEntry.kt | 3 +-- .../features/lockscreen/impl/unlock/PinUnlockPresenter.kt | 3 ++- .../featureflag/impl/PreferencesFeatureFlagProvider.kt | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 76331bd38f..92dda869a6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -18,11 +18,10 @@ package io.element.android.features.lockscreen.impl.pin.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList -import java.io.Serializable data class PinEntry( val digits: ImmutableList, -): Serializable { +) { companion object { fun empty(size: Int): PinEntry { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index d6b77f799c..a6f06158d4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.lockscreen.api.LockScreenStateService @@ -37,7 +38,7 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { - var pinEntry by rememberSaveable { + var pinEntry by remember { mutableStateOf(PinEntry.empty(4)) } var remainingAttempts by rememberSaveable { diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index 23ff977da2..ddffdebd34 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -24,7 +24,6 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.featureflag.api.Feature -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -45,10 +44,10 @@ class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext con } } - override fun isFeatureEnabled(feature: Feature): Flow { + override suspend fun isFeatureEnabled(feature: Feature): Boolean { return store.data.map { prefs -> prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue - } + }.first() } override fun hasFeature(feature: Feature): Boolean { From ab13c5d696c6aef5b414148d3e8c0979bb3b9fd8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 20 Oct 2023 20:38:27 +0200 Subject: [PATCH 107/281] Fix some warnings --- .../lockscreen/impl/setup/SetupPinPresenter.kt | 2 +- .../features/lockscreen/impl/setup/SetupPinState.kt | 3 +-- .../lockscreen/impl/setup/SetupPinStateProvider.kt | 2 +- .../features/lockscreen/impl/setup/SetupPinView.kt | 6 +++--- .../{CreatePinFailure.kt => SetupPinFailure.kt} | 0 .../impl/state/DefaultLockScreenStateService.kt | 2 +- .../features/lockscreen/impl/unlock/PinUnlockView.kt | 4 ++-- .../lockscreen/impl/unlock/numpad/PinKeypad.kt | 12 +++++++----- 8 files changed, 16 insertions(+), 15 deletions(-) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/{CreatePinFailure.kt => SetupPinFailure.kt} (100%) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 8561e5333c..6a880a6967 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -95,7 +95,7 @@ class SetupPinPresenter @Inject constructor( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - SetupPinFailure = setupPinFailure, + setupPinFailure = setupPinFailure, appName = buildMeta.applicationName, eventSink = ::handleEvents ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt index 7823b1e39f..3ae4a2c85b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt @@ -23,11 +23,10 @@ data class SetupPinState( val choosePinEntry: PinEntry, val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, - val SetupPinFailure: SetupPinFailure?, + val setupPinFailure: SetupPinFailure?, val appName: String, val eventSink: (SetupPinEvents) -> Unit ) { - val pinSize = choosePinEntry.size val activePinEntry = if (isConfirmationStep) { confirmPinEntry } else { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt index 37a232dc11..1a177b4a83 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -54,7 +54,7 @@ fun aSetupPinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - SetupPinFailure = creationFailure, + setupPinFailure = creationFailure, appName = "Element", eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index 18be5e14b4..b8f40b06d0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -114,11 +114,11 @@ private fun SetupPinContent( .padding(top = 36.dp) .fillMaxWidth() ) - if (state.SetupPinFailure != null) { + if (state.setupPinFailure != null) { ErrorDialog( modifier = modifier, - title = state.SetupPinFailure.title(), - content = state.SetupPinFailure.content(), + title = state.setupPinFailure.title(), + content = state.setupPinFailure.content(), onDismiss = { state.eventSink(SetupPinEvents.ClearFailure) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt similarity index 100% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/CreatePinFailure.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt index a071de06e9..f2e037b111 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject -private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L +//private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 2fff711131..be43d6cf32 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -127,9 +127,9 @@ fun PinUnlockView( @Composable fun PinUnlockCompactView( - modifier: Modifier = Modifier, header: @Composable () -> Unit, footer: @Composable () -> Unit, + modifier: Modifier = Modifier, content: @Composable BoxWithConstraintsScope.() -> Unit, ) { Row(modifier = modifier) { @@ -151,9 +151,9 @@ fun PinUnlockCompactView( @Composable fun PinUnlockExpandedView( - modifier: Modifier = Modifier, header: @Composable () -> Unit, footer: @Composable () -> Unit, + modifier: Modifier = Modifier, content: @Composable BoxWithConstraintsScope.() -> Unit, ) { Column( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index ee8adb19b2..8a6230c725 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -46,6 +46,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toSp import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf private val spaceBetweenPinKey = 8.dp private val maxSizePinKey = 80.dp @@ -74,28 +76,28 @@ fun PinKeypad( pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), + models = persistentListOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), onClick = onClick, ) PinKeypadRow( pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), + models = persistentListOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), onClick = onClick, ) PinKeypadRow( pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), + models = persistentListOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), onClick = onClick, ) PinKeypadRow( pinKeySize = pinKeySize, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, - models = listOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), + models = persistentListOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), onClick = onClick, ) } @@ -103,7 +105,7 @@ fun PinKeypad( @Composable private fun PinKeypadRow( - models: List, + models: ImmutableList, onClick: (PinKeypadModel) -> Unit, pinKeySize: Dp, modifier: Modifier = Modifier, From 5adcaef5694c725355f3228b68ec4dfb456b344b Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 20 Oct 2023 21:08:44 +0100 Subject: [PATCH 108/281] Add new ui for room notifications screen - Add new ui for room notifications screen - Fix error when changing default notification setting. --- .../android/appnav/LoggedInFlowNode.kt | 16 +++- .../android/appnav/room/RoomLoadedFlowNode.kt | 22 +++-- .../android/appnav/RoomFlowNodeTest.kt | 25 ++++-- features/preferences/api/build.gradle.kts | 1 + .../preferences/api/PreferencesEntryPoint.kt | 13 +++ .../impl/DefaultPreferencesEntryPoint.kt | 10 +++ .../preferences/impl/PreferencesFlowNode.kt | 2 +- ...EditDefaultNotificationSettingPresenter.kt | 8 +- .../roomdetails/api/RoomDetailsEntryPoint.kt | 14 ++- features/roomdetails/impl/build.gradle.kts | 1 + .../impl/DefaultRoomDetailsEntryPoint.kt | 26 ++++-- .../roomdetails/impl/RoomDetailsFlowNode.kt | 12 ++- .../RoomNotificationSettingsNode.kt | 11 ++- .../RoomNotificationSettingsStateProvider.kt | 12 +++ .../RoomNotificationSettingsView.kt | 85 ++++++++++++------- .../RustNotificationSettingsService.kt | 10 ++- 16 files changed, 205 insertions(+), 63 deletions(-) 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 891301532c..8d2edd0843 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -197,7 +197,9 @@ class LoggedInFlowNode @AssistedInject constructor( ) : NavTarget @Parcelize - data object Settings : NavTarget + data class Settings( + val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root + ) : NavTarget @Parcelize data object CreateRoom : NavTarget @@ -227,7 +229,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onSettingsClicked() { - backstack.push(NavTarget.Settings) + backstack.push(NavTarget.Settings()) } override fun onCreateRoomClicked() { @@ -260,11 +262,15 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onForwardedToSingleRoom(roomId: RoomId) { coroutineScope.launch { attachRoom(roomId) } } + + override fun onOpenGlobalNotificationSettings() { + backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) + } } val inputs = RoomFlowNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement) createNode(buildContext, plugins = listOf(inputs, callback)) } - NavTarget.Settings -> { + is NavTarget.Settings -> { val callback = object : PreferencesEntryPoint.Callback { override fun onOpenBugReport() { plugins().forEach { it.onOpenBugReport() } @@ -278,7 +284,9 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomNotificationSettings)) } } - preferencesEntryPoint.nodeBuilder(this, buildContext) + val inputs = PreferencesEntryPoint.Params(navTarget.initialElement) + return preferencesEntryPoint.nodeBuilder(this, buildContext) + .params(inputs) .callback(callback) .build() } 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 fa583645b2..613ed650c8 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 @@ -75,6 +75,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( interface Callback : Plugin { fun onForwardedToSingleRoom(roomId: RoomId) + fun onOpenGlobalNotificationSettings() } data class Inputs( @@ -128,6 +129,18 @@ class RoomLoadedFlowNode @AssistedInject constructor( } } + private fun createRoomDetailsNode(buildContext: BuildContext, initialTarget: RoomDetailsEntryPoint.InitialTarget): Node { + val callback = object : RoomDetailsEntryPoint.Callback { + override fun onOpenGlobalNotificationSettings() { + callbacks.forEach { it.onOpenGlobalNotificationSettings() } + } + } + return roomDetailsEntryPoint.nodeBuilder(this, buildContext) + .params(RoomDetailsEntryPoint.Params(initialTarget)) + .callback(callback) + .build() + } + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Messages -> { @@ -147,16 +160,13 @@ class RoomLoadedFlowNode @AssistedInject constructor( messagesEntryPoint.createNode(this, buildContext, callback) } NavTarget.RoomDetails -> { - val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomDetails) - roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList()) + createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomDetails) } is NavTarget.RoomMemberDetails -> { - val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(navTarget.userId)) - roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList()) + createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(navTarget.userId)) } NavTarget.RoomNotificationSettings -> { - val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings) - roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList()) + createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings) } } } 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 a25e4d8134..c05ddb6c73 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -31,6 +31,7 @@ import io.element.android.appnav.room.RoomLoadedFlowNode import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.childNode +import io.element.android.libraries.architecture.createNode 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.room.FakeMatrixRoom @@ -71,14 +72,22 @@ class RoomFlowNodeTest { var nodeId: String? = null - override fun createNode( - parentNode: Node, - buildContext: BuildContext, - inputs: RoomDetailsEntryPoint.Inputs, - plugins: List - ): Node { - return node(buildContext) {}.also { - nodeId = it.id + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder { + return object : RoomDetailsEntryPoint.NodeBuilder { + + override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder { + return this + } + + override fun callback(callback: RoomDetailsEntryPoint.Callback): RoomDetailsEntryPoint.NodeBuilder { + return this + } + + override fun build(): Node { + return node(buildContext) {}.also { + nodeId = it.id + } + } } } } diff --git a/features/preferences/api/build.gradle.kts b/features/preferences/api/build.gradle.kts index c20fe9aabb..0278385ab3 100644 --- a/features/preferences/api/build.gradle.kts +++ b/features/preferences/api/build.gradle.kts @@ -15,6 +15,7 @@ */ plugins { id("io.element.android-library") + id("kotlin-parcelize") } android { 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 50a605efe4..a0d2b8e057 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 @@ -16,17 +16,30 @@ package io.element.android.features.preferences.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 io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.parcelize.Parcelize interface PreferencesEntryPoint : FeatureEntryPoint { + sealed interface InitialTarget : Parcelable { + @Parcelize + data object Root : InitialTarget + @Parcelize + data object NotificationSettings : InitialTarget + } + + data class Params(val initialElement: InitialTarget) : NodeInputs fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { + + fun params(params: Params): NodeBuilder fun callback(callback: Callback): NodeBuilder fun build(): Node } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt index aa286394dc..e551d9d8dc 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/DefaultPreferencesEntryPoint.kt @@ -31,6 +31,11 @@ class DefaultPreferencesEntryPoint @Inject constructor() : PreferencesEntryPoint return object : PreferencesEntryPoint.NodeBuilder { val plugins = ArrayList() + override fun params(params: PreferencesEntryPoint.Params): PreferencesEntryPoint.NodeBuilder { + plugins += params + return this + } + override fun callback(callback: PreferencesEntryPoint.Callback): PreferencesEntryPoint.NodeBuilder { plugins += callback return this @@ -42,3 +47,8 @@ class DefaultPreferencesEntryPoint @Inject constructor() : PreferencesEntryPoint } } } + +internal fun PreferencesEntryPoint.InitialTarget.toNavTarget() = when (this) { + is PreferencesEntryPoint.InitialTarget.Root -> PreferencesFlowNode.NavTarget.Root + is PreferencesEntryPoint.InitialTarget.NotificationSettings -> PreferencesFlowNode.NavTarget.NotificationSettings +} 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 8d77757527..0fcf04d1df 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 @@ -53,7 +53,7 @@ class PreferencesFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Root, + initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt index 79201e27d3..5fb34687d8 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -135,7 +135,13 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( suspend { // On modern clients, we don't have different settings for encrypted and non-encrypted rooms (Legacy clients did). notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = true, mode = mode, isOneToOne = isOneToOne).getOrThrow() - notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne).getOrThrow() + val result = notificationSettingsService.setDefaultRoomNotificationMode(isEncrypted = false, mode = mode, isOneToOne = isOneToOne) + + if (result.isFailure) { + result.exceptionOrNull()?.printStackTrace() + } + + result.getOrThrow() }.runCatchingUpdatingState(action) } diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt index ed1e6f5c5a..0aaac324da 100644 --- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt +++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt @@ -38,7 +38,17 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { data object RoomNotificationSettings : InitialTarget } - data class Inputs(val initialElement: InitialTarget) : NodeInputs + data class Params(val initialElement: InitialTarget) : NodeInputs - fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs, plugins: List): Node + interface Callback : Plugin { + fun onOpenGlobalNotificationSettings() + } + + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder } diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 3bf38d4fa7..a2fdfa18c1 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.permissions.api) + implementation(projects.libraries.preferences.api) api(projects.features.roomdetails.api) api(projects.libraries.usersearch.api) api(projects.services.apperror.api) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt index 8cd6cb54d6..108ef088f0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt @@ -29,13 +29,25 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint { - override fun createNode( - parentNode: Node, - buildContext: BuildContext, - inputs: RoomDetailsEntryPoint.Inputs, - plugins: List - ): Node { - return parentNode.createNode(buildContext, plugins + inputs) + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder { + return object : RoomDetailsEntryPoint.NodeBuilder { + val plugins = ArrayList() + + override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder { + plugins += params + return this + } + + override fun callback(callback: RoomDetailsEntryPoint.Callback): RoomDetailsEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 6945ebb0ca..dba31ab223 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -23,6 +23,7 @@ import com.bumble.appyx.core.composable.Children 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.push import dagger.assisted.Assisted @@ -47,7 +48,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BackstackNode( backstack = BackStack( - initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), + initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -125,8 +126,13 @@ class RoomDetailsFlowNode @AssistedInject constructor( } is NavTarget.RoomNotificationSettings -> { - val plugins = listOf(RoomNotificationSettingsNode.RoomNotificationSettingInput(navTarget.showUserDefinedSettingStyle)) - createNode(buildContext, plugins) + val input = RoomNotificationSettingsNode.RoomNotificationSettingInput(navTarget.showUserDefinedSettingStyle) + val callback = object : RoomNotificationSettingsNode.Callback { + override fun openGlobalNotificationSettings() { + plugins().forEach { it.onOpenGlobalNotificationSettings() } + } + } + createNode(buildContext, listOf(input, callback)) } is NavTarget.RoomMemberDetails -> { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt index cb0168a42b..173e3b6af1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsNode.kt @@ -22,6 +22,7 @@ import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen @@ -42,8 +43,15 @@ class RoomNotificationSettingsNode @AssistedInject constructor( data class RoomNotificationSettingInput( val showUserDefinedSettingStyle: Boolean ) : NodeInputs - + interface Callback : Plugin { + fun openGlobalNotificationSettings() + } private val inputs = inputs() + private val callbacks = plugins() + + private fun openGlobalNotificationSettings() { + callbacks.forEach { it.openGlobalNotificationSettings() } + } init { lifecycle.subscribe( @@ -66,6 +74,7 @@ class RoomNotificationSettingsNode @AssistedInject constructor( RoomNotificationSettingsView( state = state, modifier = modifier, + onShowGlobalNotifications = this::openGlobalNotificationSettings, onBackPressed = this::navigateUp, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt index 961909f933..1a95530d29 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt @@ -36,5 +36,17 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider< restoreDefaultAction = Async.Uninitialized, eventSink = { }, ), + RoomNotificationSettingsState( + roomName = "Room 1", + Async.Success(RoomNotificationSettings( + mode = RoomNotificationMode.MUTE, + isDefault = false)), + pendingRoomNotificationMode = null, + pendingSetDefault = null, + defaultRoomNotificationMode = RoomNotificationMode.ALL_MESSAGES, + setNotificationSettingAction = Async.Uninitialized, + restoreDefaultAction = Async.Uninitialized, + eventSink = { }, + ), ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt index 3d95e4ec20..21bc11781d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -21,10 +21,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.roomdetails.impl.R @@ -34,9 +38,9 @@ import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch -import io.element.android.libraries.designsystem.components.preferences.PreferenceText 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.aliasScreenTitle import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text @@ -49,6 +53,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun RoomNotificationSettingsView( state: RoomNotificationSettingsState, modifier: Modifier = Modifier, + onShowGlobalNotifications: () -> Unit = {}, onBackPressed: () -> Unit = {}, ) { Scaffold( @@ -66,40 +71,62 @@ fun RoomNotificationSettingsView( .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - val subtitle = when (state.defaultRoomNotificationMode) { - RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_room_notification_settings_mode_all_messages) - RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> stringResource(id = R.string.screen_room_notification_settings_mode_mentions_and_keywords) - RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute) - null -> "" - } - val roomNotificationSettings = state.roomNotificationSettings.dataOrNull() - - PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) { - PreferenceSwitch( - isChecked = state.displayIsDefault.orTrue(), - onCheckedChange = { - state.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(it)) - }, - title = "Match default setting", - subtitle = subtitle, - enabled = roomNotificationSettings != null - ) - - PreferenceText( - title = stringResource(id = R.string.screen_room_notification_settings_allow_custom), - subtitle = stringResource(id = R.string.screen_room_notification_settings_allow_custom_footnote), - enabled = !state.displayIsDefault.orTrue(), - ) - - if (roomNotificationSettings != null && state.displayNotificationMode != null) { + PreferenceSwitch( + isChecked = !state.displayIsDefault.orTrue(), + onCheckedChange = { + state.eventSink(RoomNotificationSettingsEvents.SetNotificationMode(!it)) + }, + title = stringResource(id = R.string.screen_room_notification_settings_allow_custom), + subtitle = stringResource(id = R.string.screen_room_notification_settings_allow_custom_footnote), + enabled = roomNotificationSettings != null + ) + if (state.displayIsDefault.orTrue()) { + PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_default_setting_title)) { + val text = buildAnnotatedStringWithStyledPart( + R.string.screen_room_notification_settings_default_setting_footnote, + R.string.screen_room_notification_settings_default_setting_footnote_content_link, + color = Color.Unspecified, + underline = false, + bold = true, + ) + ClickableText( + text = text, + onClick = { + onShowGlobalNotifications() + }, + modifier = Modifier + .padding(start = 16.dp, bottom = 16.dp, end = 16.dp), + style = ElementTheme.typography.fontBodyMdRegular + .copy( + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center, + ) + ) + if(state.defaultRoomNotificationMode != null){ + val defaultModeTitle = when (state.defaultRoomNotificationMode) { + RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_room_notification_settings_mode_all_messages) + RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> { + stringResource(id = R.string.screen_room_notification_settings_mode_mentions_and_keywords) + } + RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute) + } + RoomNotificationSettingsOption( + roomNotificationSettingsItem = RoomNotificationSettingsItem(state.defaultRoomNotificationMode, defaultModeTitle), + isSelected = true, + onOptionSelected = { }, + enabled = true + ) + } + } + } else { + PreferenceCategory(title = stringResource(id = R.string.screen_room_notification_settings_custom_settings_title)) { RoomNotificationSettingsOptions( selected = state.displayNotificationMode, enabled = !state.displayIsDefault.orTrue(), onOptionSelected = { state.eventSink(RoomNotificationSettingsEvents.RoomNotificationModeChanged(it.mode)) - }, - ) + },) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt index df5c4d577f..42b2662a6d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -28,6 +28,8 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.NotificationSettings import org.matrix.rustcomponents.sdk.NotificationSettingsDelegate +import org.matrix.rustcomponents.sdk.NotificationSettingsException +import timber.log.Timber class RustNotificationSettingsService( private val notificationSettings: NotificationSettings, @@ -63,7 +65,13 @@ class RustNotificationSettingsService( isOneToOne: Boolean ): Result = withContext(dispatchers.io) { runCatching { - notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) + try { + notificationSettings.setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) + } catch (exception: NotificationSettingsException.RuleNotFound) { + // `setDefaultRoomNotificationMode` updates multiple rules including unstable rules (e.g. the polls push rules defined in the MSC3930) + // since production home servers may not have these rules yet, we drop the RuleNotFound error + Timber.w("Unable to find the rule: ${exception.ruleId}") + } } } From ea2c332e52bceb91314c43bbb79287f19c7d1c7f Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 20 Oct 2023 20:30:14 +0000 Subject: [PATCH 109/281] Update screenshots --- ...ltNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ltNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png | 4 ++-- ..._RoomNotificationSettings-D-4_4_null_0,NEXUS_5,1.0,en].png | 4 ++-- ..._RoomNotificationSettings-D-4_4_null_1,NEXUS_5,1.0,en].png | 3 +++ ..._RoomNotificationSettings-N-4_5_null_0,NEXUS_5,1.0,en].png | 4 ++-- ..._RoomNotificationSettings-N-4_5_null_1,NEXUS_5,1.0,en].png | 3 +++ 6 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-D-4_4_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_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_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png index 1d4750eecb..7e1e8e88a0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-D-8_8_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1172d81259b7e7b694f428c5144d96c8a7748134f5b4f515a5c25c58ff8e5e1b -size 31318 +oid sha256:11aa649bb8e25975c79ebc8c20aa82153acac75b4087f99485c58bd604ba3f33 +size 35947 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png index c423fe4456..6e3219983b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e835ab12c0a77c4a4dcaf8bac0d78ed5ec9a0ab8eef669c7d814317ab06abd27 -size 28443 +oid sha256:1df0f8620db5a3f751e37543f34785517703e4731af9e624dc50b91de79f46e9 +size 33107 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-D-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-D-4_4_null_0,NEXUS_5,1.0,en].png index 88554d0d72..7a08009659 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-D-4_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-D-4_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f34dde086bbd14cc5cfeb0003d6bfd0d3868ecbc497956dc17794e69f6773279 -size 40070 +oid sha256:3f2fc33febab98860da9b6591c4ae33dc9ba4fa28365520e4fcb08f8bfaf6ff8 +size 33828 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-D-4_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-D-4_4_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..383adaff52 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-D-4_4_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d99786d84445bc42f7e7f7e414f973a04ba6684780f69eb1cfe60a09ff3ec6e +size 38017 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_0,NEXUS_5,1.0,en].png index 9adff83203..d59805dfbd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0545d55a8a78af5d8286fc590998c973cee986a3f6aff91850cf277d47a5e5 -size 36415 +oid sha256:0cf8d93229fd5d8034dc7e2f68f9868fc42515c538098c89d4dddca814827bc3 +size 31448 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9b7a1f3917 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13c5007d3a97734dca40a88f2312e59d09a8906deb71d16c0611df7dd25a6391 +size 35124 From 244c45797fb40089d4c76c0d35cde9007f92c949 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 22:32:59 +0200 Subject: [PATCH 110/281] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.63 (#1619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency org.matrix.rustcomponents:sdk-android to v0.1.63 * Update Element Call integrated APIs * Take into account the new `MessageType.Other` from the SDK --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín Co-authored-by: Benoit Marty --- .../event/TimelineItemContentMessageFactory.kt | 9 ++++++++- gradle/libs.versions.toml | 2 +- .../impl/DefaultRoomLastMessageFormatter.kt | 7 ++++++- .../impl/DefaultRoomLastMessageFormatterTest.kt | 4 ++++ .../matrix/api/timeline/item/event/MessageType.kt | 5 +++++ .../libraries/matrix/impl/room/RustMatrixRoom.kt | 10 +++++----- .../impl/timeline/item/event/EventMessageMapper.kt | 5 +++++ .../impl/widget/DefaultCallWidgetSettingsProvider.kt | 2 +- .../matrix/impl/widget/MatrixWidgetSettings.kt | 4 ++-- .../libraries/matrix/impl/widget/RustWidgetDriver.kt | 6 +++--- .../push/impl/notifications/NotifiableEventResolver.kt | 2 ++ 11 files changed, 42 insertions(+), 14 deletions(-) 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 323f110f47..f39c1e0989 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 @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageT import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType @@ -131,8 +132,14 @@ class TimelineItemContentMessageFactory @Inject constructor( htmlDocument = messageType.formatted?.toHtmlDocument(), isEdited = content.isEdited, ) + is OtherMessageType -> TimelineItemTextContent( + body = messageType.body, + htmlDocument = null, + isEdited = content.isEdited, + ) UnknownMessageType -> TimelineItemTextContent( - // Display the body as a fallback + // Display the body as a fallback, but should not happen anymore + // (we have `OtherMessageType` now) body = content.body, htmlDocument = null, isEdited = content.isEdited, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 23aaa4327b..807adf020c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -148,7 +148,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.62" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.63" 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" } 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 d041078051..80e7a7155b 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 @@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails @@ -129,8 +130,12 @@ class DefaultRoomLastMessageFormatter @Inject constructor( is AudioMessageType -> { sp.getString(CommonStrings.common_audio) } + is OtherMessageType -> { + messageType.body + } UnknownMessageType -> { - // Display the body as a fallback + // Display the body as a fallback, but should not happen anymore + // (we have `OtherMessageType` now) messageContent.body } is NoticeMessageType -> { 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 50d313f132..bc58aa1d48 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 @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent @@ -204,6 +205,7 @@ class DefaultRoomLastMessageFormatterTest { is EmoteMessageType -> "* $senderName ${type.body}" is TextMessageType, is NoticeMessageType, + is OtherMessageType, UnknownMessageType -> body } Truth.assertWithMessage("$type was not properly handled for DM").that(result).isEqualTo(expectedResult) @@ -220,6 +222,7 @@ class DefaultRoomLastMessageFormatterTest { is LocationMessageType -> "$senderName: Shared location" is TextMessageType, is NoticeMessageType, + is OtherMessageType, UnknownMessageType -> "$senderName: $body" is EmoteMessageType -> "* $senderName ${type.body}" } @@ -231,6 +234,7 @@ class DefaultRoomLastMessageFormatterTest { is LocationMessageType -> false is EmoteMessageType -> false is TextMessageType, is NoticeMessageType -> true + is OtherMessageType -> true UnknownMessageType -> true } if (shouldCreateAnnotatedString) { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt index ba6eeca819..09f0c00a7c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt @@ -73,3 +73,8 @@ data class TextMessageType( val body: String, val formatted: FormattedBody? ) : MessageType + +data class OtherMessageType( + val msgType: String, + val body: String, +) : MessageType 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 c778fa3ac5..8dd0c02321 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 @@ -69,8 +69,8 @@ import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle -import org.matrix.rustcomponents.sdk.WidgetPermissions -import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider +import org.matrix.rustcomponents.sdk.WidgetCapabilities +import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import timber.log.Timber @@ -497,9 +497,9 @@ class RustMatrixRoom( RustWidgetDriver( widgetSettings = widgetSettings, room = innerRoom, - widgetPermissionsProvider = object : WidgetPermissionsProvider { - override fun acquirePermissions(permissions: WidgetPermissions): WidgetPermissions { - return permissions + widgetCapabilitiesProvider = object : WidgetCapabilitiesProvider { + override fun acquireCapabilities(capabilities: WidgetCapabilities): WidgetCapabilities { + return capabilities } }, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 18d2e1bdeb..521ff3bd5a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -28,11 +28,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.impl.media.map import org.matrix.rustcomponents.sdk.Message +import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.ProfileDetails import org.matrix.rustcomponents.sdk.RepliedToEventDetails import org.matrix.rustcomponents.sdk.use @@ -104,6 +106,9 @@ class EventMessageMapper { is RustMessageType.Location -> { LocationMessageType(type.content.body, type.content.geoUri, type.content.description) } + is MessageType.Other -> { + OtherMessageType(type.msgtype, type.body) + } null -> UnknownMessageType } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt index a7f208e69d..1a34a31167 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt @@ -37,7 +37,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor() : CallWidgetSettin appPrompt = false, skipLobby = true, confineToRoom = true, - fonts = null, + font = null, analyticsId = null ) val rustWidgetSettings = newVirtualElementCallWidget(options) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt index 65e6c8bc84..018e02816c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/MatrixWidgetSettings.kt @@ -23,13 +23,13 @@ import org.matrix.rustcomponents.sdk.WidgetSettings import org.matrix.rustcomponents.sdk.generateWebviewUrl fun MatrixWidgetSettings.toRustWidgetSettings() = WidgetSettings( - id = this.id, + widgetId = this.id, initAfterContentLoad = this.initAfterContentLoad, rawUrl = this.rawUrl, ) fun MatrixWidgetSettings.Companion.fromRustWidgetSettings(widgetSettings: WidgetSettings) = MatrixWidgetSettings( - id = widgetSettings.id, + id = widgetSettings.widgetId, initAfterContentLoad = widgetSettings.initAfterContentLoad, rawUrl = widgetSettings.rawUrl, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt index e385a34e0d..2764cecfdc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.matrix.rustcomponents.sdk.Room -import org.matrix.rustcomponents.sdk.WidgetPermissionsProvider +import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.makeWidgetDriver import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.coroutineContext @@ -33,7 +33,7 @@ import kotlin.coroutines.coroutineContext class RustWidgetDriver( widgetSettings: MatrixWidgetSettings, private val room: Room, - private val widgetPermissionsProvider: WidgetPermissionsProvider, + private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider, ): MatrixWidgetDriver { override val incomingMessages = MutableSharedFlow() @@ -54,7 +54,7 @@ class RustWidgetDriver( val coroutineScope = CoroutineScope(coroutineContext) coroutineScope.launch { // This call will suspend the coroutine while the driver is running, so it needs to be launched separately - driverAndHandle.driver.run(room, widgetPermissionsProvider) + driverAndHandle.driver.run(room, widgetCapabilitiesProvider) } receiveMessageJob = coroutineScope.launch(Dispatchers.IO) { try { 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 113a323611..aabc58befb 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 @@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType @@ -216,6 +217,7 @@ class NotifiableEventResolver @Inject constructor( is TextMessageType -> messageType.body is VideoMessageType -> messageType.body is LocationMessageType -> messageType.body + is OtherMessageType -> messageType.body is UnknownMessageType -> stringProvider.getString(CommonStrings.common_unsupported_event) } } From 94c5807f4098fbba8bd4b57e4bd3d865785473f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:36:15 +0200 Subject: [PATCH 111/281] Update dependency org.owasp.dependencycheck to v8.4.2 (#1622) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 807adf020c..50de2438c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ serialization_json = "1.6.0" showkase = "1.0.0-beta18" jsoup = "1.16.2" appyx = "1.4.0" -dependencycheck = "8.4.0" +dependencycheck = "8.4.2" dependencyanalysis = "1.25.0" stem = "2.3.0" sqldelight = "2.0.0" From 0e00797e67c08ad486279680c2d185953c01315a Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 23 Oct 2023 08:15:26 +0000 Subject: [PATCH 112/281] Update screenshots --- ...null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png | 3 --- ...nents_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png | 3 +++ ...nents_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png | 3 +++ ...l.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png} | 0 ...unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png | 3 +++ ...unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png | 3 +++ ....unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png | 3 +++ 26 files changed, 42 insertions(+), 6 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index f1992899f6..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f19d53c688e3f862894775756f00040adb5cbba99de71c053ed503c1b8af9518 -size 15239 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index f7a21c8025..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11c145595f7713bc7b66f9d07e917bca82d6e06a1b55367801eb4d2cbfef89b0 -size 14353 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2f7ce4747 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1d44f62edd3f2d30421e49ad435971b165711b8bfe6d4a9475c5fb2f9f83ed +size 8506 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a4c7739624 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d58d2d25d8f2c07c976bcda0eea7ec101f993ad1ef733fae4c713a67650e33e2 +size 8498 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-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.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-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.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-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.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-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.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fc54ab5f7e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b14ff41b74e36809f359a7b3b73ffdb9c0c2cd97ec786333bcce9412f05825e6 +size 30608 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..62b7c73a26 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1521d9fecaaadcfc7ed4670d997b36a20bd2a84f564794a06cb10bfe7d1ad64e +size 28812 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e81bbf1c1d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c369763c4be676d6727a6b0f5cd74d56a9de09b9c3780e1806874d19ba7ffd3f +size 42705 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bcb90d6b4a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a63439f320a1b6ad124cb943f95651f85d2e9cfc34597cb450c803b6dafaefa7 +size 43149 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bf2823f6a4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:165e37ef78a7b09bbd49e8d692ccb27836e35d8535b182d9837a5c3daa5cf3a6 +size 43978 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b3ca08978d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecaba43c72a890d85f9894eb987f291967c8ea15bdc09db88d6a72384098d827 +size 48308 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5b8c62dcbf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25175242be4133a74541f0bb2d05db95484a65da952031fb8f68a2fdef88a995 +size 45880 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fc34314f30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:699c96cc0d06c85eeda3a3b408877d37445bebf1873a86d6255ba8626ba410b0 +size 39654 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6d029a3034 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:233bb8cb261f752ac868c492d075fea9e5af656fcc8ad0d12fbf05b2dd690788 +size 40113 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af181f44e5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:920b9f28c3d2f5e726ad4beeb668e75c285b0e731e6d7eaaf87ca3b2a8b9980b +size 40652 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2616989dcd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28197831dacc61f25d6b3a0080db331ef317283e8032fa5cc4ed457709da0f89 +size 42980 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ce7df11d83 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652154d82c5f43f91cface3f2e5650ee5a98cf1faed08a80589ab42225bd8120 +size 40557 From c8dc364147f53feb38499c975065a17770404f1d Mon Sep 17 00:00:00 2001 From: ElementBot <110224175+ElementBot@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:20:49 +0100 Subject: [PATCH 113/281] Sync Strings (#1623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync Strings from Localazy * Fix strings * Update screenshots --------- Co-authored-by: bmarty Co-authored-by: Jorge Martín --- .../src/main/res/values-sk/translations.xml | 26 +++++++++ .../src/main/res/values-sk/translations.xml | 4 ++ .../api/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-ru/translations.xml | 1 + .../src/main/res/values-sk/translations.xml | 3 ++ .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 4 +- .../src/main/res/values-ru/translations.xml | 5 ++ .../src/main/res/values-sk/translations.xml | 53 +++++++++++++++++++ .../main/res/values-zh-rTW/translations.xml | 1 + .../src/main/res/values/localazy.xml | 2 + ...ationHeader-D-3_3_null,NEXUS_5,1.0,en].png | 4 +- ...ationHeader-N-3_4_null,NEXUS_5,1.0,en].png | 4 +- ...mListView-D-2_2_null_1,NEXUS_5,1.0,en].png | 4 +- ...mListView-N-2_3_null_1,NEXUS_5,1.0,en].png | 4 +- 15 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 features/lockscreen/impl/src/main/res/values-sk/translations.xml diff --git a/features/lockscreen/impl/src/main/res/values-sk/translations.xml b/features/lockscreen/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..593542d81e --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,26 @@ + + + + "Nesprávny PIN kód. Máte ešte %1$d pokus" + "Nesprávny PIN kód. Máte ešte %1$d pokusy" + "Nesprávny PIN kód. Máte ešte %1$d pokusov" + + "Zabudli ste PIN?" + "Zmeniť PIN kód" + "Povoliť biometrické odomknutie" + "Odstrániť PIN" + "Ste si istí, že chcete odstrániť PIN?" + "Odstrániť PIN?" + "Vyberte PIN" + "Potvrdiť PIN" + "Z bezpečnostných dôvodov si nemôžete toto zvoliť ako svoj PIN kód." + "Vyberte iný PIN" + "Uzamknite %1$s, aby ste zvýšili bezpečnosť svojich konverzácií. + +Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplikácie odhlásení." + "Zadajte prosím ten istý PIN dvakrát" + "PIN kódy sa nezhodujú" + "Ak chcete pokračovať, musíte sa znovu prihlásiť a vytvoriť nový PIN kód." + "Prebieha odhlasovanie" + "Máte 3 pokusy na odomknutie" + diff --git a/features/logout/api/src/main/res/values-sk/translations.xml b/features/logout/api/src/main/res/values-sk/translations.xml index 212f11ccbc..2c334e9c3b 100644 --- a/features/logout/api/src/main/res/values-sk/translations.xml +++ b/features/logout/api/src/main/res/values-sk/translations.xml @@ -1,8 +1,12 @@ + "Prosím, počkajte na dokončenie tohto kroku a až potom sa odhláste." + "Vaše kľúče sa ešte stále zálohujú" "Ste si istí, že sa chcete odhlásiť?" "Odhlásiť sa" "Prebieha odhlasovanie…" + "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam." + "Uložili ste kľúč na obnovenie?" "Odhlásiť sa" "Odhlásiť sa" diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index 5a5c9c64ca..c695309194 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -6,7 +6,7 @@ "Sign out" "Signing out…" "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." - "Have you saved your recovery key?" + "Recovery not set up" "Sign out" "Sign out" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index e54b92ff08..4bb44db372 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -38,5 +38,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 392999558e..2e1dc22b93 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,8 @@ + "Vlastná Element Call základná URL adresa" + "Nastaviť vlastnú základnú URL adresu pre Element Call." + "Neplatná adresa URL, uistite sa, že ste uviedli protokol (http/https) a správnu adresu." "Vývojársky režim" "Umožniť prístup k možnostiam a funkciám pre vývojárov." "Vypnite rozšírený textový editor na ručné písanie Markdown." diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index d63f96d07c..26b0bbee9e 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -4,6 +4,6 @@ "Get started by messaging someone." "No chats yet." "All Chats" - "Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards." + "Looks like you’re using a new device. Verify with another device to access your encrypted messages." "Verify it’s you" 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 7ada336f28..e5f46af0e6 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -207,9 +207,9 @@ "Další nastavení" "Halsové a video hovory" "Neshoda konfigurace" - "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. + "Zjednodušili jsme nastavení oznámení, abychom usnadnili hledání možností. -Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. +Některá vlastní nastavení, která jste si vybrali v minulosti, se zde nezobrazují, ale jsou stále aktivní. Pokud budete pokračovat, některá nastavení se mohou změnit." "Přímé zprávy" 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 ada42316b1..64d233e598 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -1,13 +1,17 @@ + "Удалить" "Скрыть пароль" "Только упоминания" "Звук отключен" + "Приостановить" + "Воспроизвести" "Опрос" "Опрос завершен" "Отправить файлы" "Показать пароль" "Меню пользователя" + "Запишите голосовое сообщение. Дважды нажмите и удерживайте, чтобы записать. Отпустите, чтобы закончить запись." "Разрешить" "Добавить в хронологию" "Назад" @@ -90,6 +94,7 @@ "%1$s%2$s" "Шифрование включено" "Ошибка" + "Для всех" "Файл" "Файл сохранен в «Загрузки»" "Переслать сообщение" 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 c6cf324b75..5cb20f8e22 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -5,6 +5,7 @@ "Iba zmienky" "Stlmené" "Pozastaviť" + "Pole PIN" "Prehrať" "Anketa" "Ukončená anketa" @@ -69,12 +70,15 @@ "Zdieľať" "Zdieľať odkaz" "Prihláste sa znova" + "Odhlásiť sa" + "Napriek tomu sa odhlásiť" "Preskočiť" "Spustiť" "Začať konverzáciu" "Spustiť overovanie" "Ťuknutím načítate mapu" "Urobiť fotku" + "Skúste to znova" "Zobraziť zdroj" "Áno" "Upraviť anketu" @@ -84,6 +88,7 @@ "Analytika" "Zvuk" "Bubliny" + "Záloha konverzácie" "Autorské práva" "Vytváranie miestnosti…" "Opustil/a miestnosť" @@ -93,6 +98,7 @@ "Upravuje sa" "* %1$s %2$s" "Šifrovanie zapnuté" + "Zadajte svoj PIN" "Chyba" "Všetci" "Súbor" @@ -122,6 +128,7 @@ "Zásady ochrany osobných údajov" "Reakcia" "Reakcie" + "Kľúč na obnovenie" "Obnovuje sa…" "Odpoveď na %1$s" "Nahlásiť chybu" @@ -129,6 +136,7 @@ "Rozšírený textový editor" "Názov miestnosti" "napr. názov vášho projektu" + "Zámok obrazovky" "Vyhľadať niekoho" "Výsledky hľadania" "Bezpečnosť" @@ -150,6 +158,7 @@ "Nie je možné dešifrovať" "Pozvánky nebolo možné odoslať jednému alebo viacerým používateľom." "Nie je možné odoslať pozvánku/ky" + "Odomknúť" "Zrušiť stlmenie zvuku" "Nepodporovaná udalosť" "Používateľské meno" @@ -174,8 +183,10 @@ "%1$s nedokázal načítať mapu. Skúste to prosím neskôr." "Načítanie správ zlyhalo" "%1$s nemohol získať prístup k vašej polohe. Skúste to prosím neskôr." + "Nepodarilo sa nahrať hlasovú správu." "%1$s nemá povolenie na prístup k vašej polohe. Prístup môžete zapnúť v Nastaveniach." "%1$s nemá povolenie na prístup k vašej polohe. Povoľte prístup nižšie." + "%1$s nemá povolenie na prístup k vášmu mikrofónu. Povoľte prístup na nahrávanie hlasovej správy." "Niektoré správy neboli odoslané" "Prepáčte, vyskytla sa chyba" "🔐️ Pripojte sa ku mne na %1$s" @@ -184,6 +195,11 @@ "Ste si istí, že chcete opustiť túto miestnosť? Táto miestnosť nie je verejná a bez pozvania sa do nej nebudete môcť vrátiť." "Ste si istí, že chcete opustiť miestnosť?" "%1$s Android" + + "%1$d zadaná číslica" + "%1$d zadané číslice" + "%1$d zadaných číslic" + "%1$d člen" "%1$d členovia" @@ -201,6 +217,22 @@ "Toto je začiatok tejto konverzácie." "Nové" "Zdieľať analytické údaje" + "Vypnúť zálohovanie" + "Zapnúť zálohovanie" + "Zálohovanie zaisťuje, že nestratíte históriu správ. %1$s." + "Zálohovanie" + "Zmeniť kľúč na obnovenie" + "Potvrdiť kľúč na obnovenie" + "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í." + "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?" + "Vypnutím zálohovania sa odstráni aktuálna záloha šifrovacích kľúčov a vypnú sa ďalšie bezpečnostné funkcie. V tomto prípade:" + "Na nových zariadeniach nebudete mať zašifrovanú históriu správ" + "Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite zo všetkých %1$s zariadení" + "Ste si istí, že chcete vypnúť zálohovanie?" "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." @@ -231,6 +263,27 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""nastavenia systému" "Systémové oznámenia sú vypnuté" "Oznámenia" + "Získajte nový kľúč na obnovenie, ak ste stratili svoj existujúci. Po zmene kľúča na obnovenie už starý kľúč nebude fungovať." + "Vygenerovať nový kľúč na obnovenie" + "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." + "Zadajte 48-znakový kód." + "Zadať…" + "Kľúč na obnovu potvrdený" + "Potvrďte kľúč na obnovenie" + "Uložiť kľúč na obnovenie" + "Zapíšte si kľúč na obnovenie na bezpečné miesto alebo ho uložte do správcu hesiel." + "Ťuknutím skopírujte kľúč na obnovenie" + "Uložte svoj kľúč na obnovenie" + "Po tomto kroku nebudete mať prístup k novému kľúču na obnovenie." + "Uložili ste kľúč na obnovenie?" + "Vaša záloha konverzácie je chránená kľúčom na obnovenie. Ak potrebujete nový kľúč na obnovenie, po nastavení si ho môžete znova vytvoriť výberom položky „Zmeniť kľúč na obnovenie“." + "Vygenerujte si váš kľúč na obnovenie" + "Uistite sa, že kľúč na obnovenie môžete uložiť niekde v bezpečí" + "Úspešné nastavenie obnovy" + "Nastaviť obnovenie" "Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa" "Zdieľať polohu" "Zdieľať moju polohu" 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 71aa40e8fb..a87c7b50f3 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 @@ -25,6 +25,7 @@ "複製連結" "建立" "建立聊天室" + "拒絕" "停用" "完成" "編輯" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 2e2668c837..c70eceb21f 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -78,6 +78,7 @@ "Start verification" "Tap to load map" "Take photo" + "Try again" "View source" "Yes" "Edit poll" @@ -182,6 +183,7 @@ "%1$s could not load the map. Please try again later." "Failed loading messages" "%1$s could not access your location. Please try again later." + "Failed to upload your voice message." "%1$s does not have permission to access your location. You can enable access in Settings." "%1$s does not have permission to access your location. Enable access below." "%1$s does not have permission to access your microphone. Enable access to record a voice message." diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png index 5b4c6141ef..db9a827165 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf2f1ba80fa68e1a1277e00a90efd157b38f367cf264b1f75969067f0b71bd65 -size 28736 +oid sha256:dca845bd952e92a37f50aced5dd4d1dbe7dcaeccecade64cd1164a20efc65200 +size 26875 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png index d0b1ef9ac2..d11cf5060a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66111b25139ada1f03833b8c3532fcb138aed48644bd7ff8a6b7e9cca3d99f35 -size 28127 +oid sha256:a9c45cd3b849a04aba6813003632b8706d0e433ad6178b7eec616647f1b950ba +size 26039 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png index 0cdf38dc42..9561e126dc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05e8a1cabb49befa77b2a89c20e83e2f8355242e8868ae075e7967c7a14ca870 -size 89054 +oid sha256:c02883f4a671a5c27da1fd5f3f635d7e23ed077aec2d47dc2ac6e0e2ced72f59 +size 86748 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png index 222acb9e53..10de4f6b65 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e4fdc7a1af79a5c2113f7c8f27f1c22463fbb3a3dbdddf83f7258ba513ea06a -size 90775 +oid sha256:2ad2f44c3fb1915ad6e226a973cecc3662c02bdf4dd8faf1eb3f056346fa1aca +size 88667 From 7048da2e203cc9577964788a9a1b61ef024701d5 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 23 Oct 2023 09:31:32 +0100 Subject: [PATCH 114/281] Fix issue where text is cleared when cancelling a reply (#1617) --- changelog.d/1617.bugfix | 1 + .../messages/impl/MessagesStateProvider.kt | 2 +- .../MessageComposerContextImpl.kt | 2 +- .../MessageComposerPresenter.kt | 10 +++--- .../MessageComposerStateProvider.kt | 2 +- .../MessageComposerPresenterTest.kt | 33 ++++++++++++++++--- .../test/MessageComposerContextFake.kt | 2 +- .../libraries/textcomposer/TextComposer.kt | 14 ++++---- .../textcomposer/components/SendButton.kt | 2 +- .../textcomposer/model/MessageComposerMode.kt | 2 +- 10 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 changelog.d/1617.bugfix diff --git a/changelog.d/1617.bugfix b/changelog.d/1617.bugfix new file mode 100644 index 0000000000..8beee812e5 --- /dev/null +++ b/changelog.d/1617.bugfix @@ -0,0 +1 @@ +Fix issue where text is cleared when cancelling a reply \ No newline at end of file diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 4222a0889d..249c4b487e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -59,7 +59,7 @@ fun aMessagesState() = MessagesState( composerState = aMessageComposerState().copy( richTextEditorState = RichTextEditorState("Hello", initialFocus = true), isFullScreen = false, - mode = MessageComposerMode.Normal("Hello"), + mode = MessageComposerMode.Normal, ), voiceMessageComposerState = aVoiceMessageComposerState(), timelineState = aTimelineState().copy( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt index c9a491d8a3..2353285499 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt @@ -29,6 +29,6 @@ import javax.inject.Inject @SingleIn(RoomScope::class) @ContributesBinding(RoomScope::class) class MessageComposerContextImpl @Inject constructor() : MessageComposerContext { - override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal("")) + override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal) internal set } 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 e4a8d50cb8..3857600e58 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 @@ -155,10 +155,12 @@ class MessageComposerPresenter @Inject constructor( when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value MessageComposerEvents.CloseSpecialMode -> { - localCoroutineScope.launch { - richTextEditorState.setHtml("") + if (messageComposerContext.composerMode is MessageComposerMode.Edit) { + localCoroutineScope.launch { + richTextEditorState.setHtml("") + } } - messageComposerContext.composerMode = MessageComposerMode.Normal("") + messageComposerContext.composerMode = MessageComposerMode.Normal } is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage( message = event.message, @@ -253,7 +255,7 @@ class MessageComposerPresenter @Inject constructor( val capturedMode = messageComposerContext.composerMode // Reset composer right away richTextEditorState.setHtml("") - updateComposerMode(MessageComposerMode.Normal("")) + updateComposerMode(MessageComposerMode.Normal) when (capturedMode) { is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html) is MessageComposerMode.Edit -> { 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 7dbe413e83..76f40a1969 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 @@ -30,7 +30,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) { + private suspend fun ReceiveTurbine.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState { state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode) skipItems(skipCount) val normalState = awaitItem() - assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal("")) - assertThat(normalState.richTextEditorState.messageHtml).isEqualTo("") + assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal) + return normalState } private fun createPresenter( diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt index 0ae604004d..03af64e071 100644 --- a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt @@ -20,5 +20,5 @@ import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.textcomposer.model.MessageComposerMode class MessageComposerContextFake( - override var composerMode: MessageComposerMode = MessageComposerMode.Normal(null) + override var composerMode: MessageComposerMode = MessageComposerMode.Normal ) : MessageComposerContext 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 18246f1ac4..911cebb142 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 @@ -483,7 +483,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { RichTextEditorState("", initialFocus = true), voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, onResetComposerMode = {}, enableTextFormatting = true, enableVoiceMessages = true, @@ -493,7 +493,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { RichTextEditorState("A message", initialFocus = true), voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, onResetComposerMode = {}, enableTextFormatting = true, enableVoiceMessages = true, @@ -506,7 +506,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { ), voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, onResetComposerMode = {}, enableTextFormatting = true, enableVoiceMessages = true, @@ -516,7 +516,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { RichTextEditorState("A message without focus", initialFocus = false), voiceMessageState = VoiceMessageState.Idle, onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, onResetComposerMode = {}, enableTextFormatting = true, enableVoiceMessages = true, @@ -533,7 +533,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview { RichTextEditorState("", initialFocus = false), voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, enableTextFormatting = true, enableVoiceMessages = true, ) @@ -542,7 +542,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview { RichTextEditorState("A message", initialFocus = false), voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, enableTextFormatting = true, enableVoiceMessages = true, ) @@ -551,7 +551,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview { RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", initialFocus = false), voiceMessageState = VoiceMessageState.Idle, showTextFormatting = true, - composerMode = MessageComposerMode.Normal(""), + composerMode = MessageComposerMode.Normal, enableTextFormatting = true, enableVoiceMessages = true, ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt index 8dc1a4706b..be258f07de 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -93,7 +93,7 @@ internal fun SendButton( @PreviewsDayNight @Composable internal fun SendButtonPreview() = ElementPreview { - val normalMode = MessageComposerMode.Normal("") + val normalMode = MessageComposerMode.Normal val editMode = MessageComposerMode.Edit(null, "", null) Row { SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index 49ce0ddb6e..34ab1641f2 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -24,7 +24,7 @@ import kotlinx.parcelize.Parcelize sealed interface MessageComposerMode : Parcelable { @Parcelize - data class Normal(val content: CharSequence?) : MessageComposerMode + data object Normal: MessageComposerMode sealed class Special(open val eventId: EventId?, open val defaultContent: String) : MessageComposerMode From 0bc992c1ba5407730b666eec2183db92b0194dba Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 10:32:43 +0200 Subject: [PATCH 115/281] Pin : fix tests after rename --- .../impl/pin/DefaultPinCodeManager.kt | 2 +- .../lockscreen/impl/pin/PinCodeManager.kt | 2 +- ...senterTest.kt => SetupPinPresenterTest.kt} | 38 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/{CreatePinPresenterTest.kt => SetupPinPresenterTest.kt} (74%) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index f5848a9d40..e7529e9280 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -37,7 +37,7 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } - override suspend fun setupPinCode(pinCode: String) { + override suspend fun createPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 09197c3eb1..5f84f5296d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -30,7 +30,7 @@ interface PinCodeManager { * Creates a new encrypted pin code. * @param pinCode the clear pin code to create */ - suspend fun setupPinCode(pinCode: String) + suspend fun createPinCode(pinCode: String) /** * @return true if the pin code is correct. diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt similarity index 74% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 723b8e8e6b..d7529be243 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -22,14 +22,14 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import io.element.android.features.lockscreen.impl.setup.validation.CreatePinFailure import io.element.android.features.lockscreen.impl.setup.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest import org.junit.Test -class CreatePinPresenterTest { +class SetupPinPresenterTest { private val blacklistedPin = PinValidator.BLACKLIST.first() private val halfCompletePin = "12" @@ -39,58 +39,58 @@ class CreatePinPresenterTest { @Test fun `present - complete flow`() = runTest { - val presenter = createCreatePinPresenter() + val presenter = createSetupPinPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().also { state -> state.choosePinEntry.assertEmpty() state.confirmPinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() + assertThat(state.setupPinFailure).isNull() assertThat(state.isConfirmationStep).isFalse() - state.eventSink(CreatePinEvents.OnPinEntryChanged(halfCompletePin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(halfCompletePin)) } awaitItem().also { state -> state.choosePinEntry.assertText(halfCompletePin) state.confirmPinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() + assertThat(state.setupPinFailure).isNull() assertThat(state.isConfirmationStep).isFalse() - state.eventSink(CreatePinEvents.OnPinEntryChanged(blacklistedPin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(blacklistedPin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(blacklistedPin) - assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinBlacklisted) - state.eventSink(CreatePinEvents.ClearFailure) + assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.PinBlacklisted) + state.eventSink(SetupPinEvents.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + assertThat(state.setupPinFailure).isNull() + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() - state.eventSink(CreatePinEvents.OnPinEntryChanged(mismatchedPin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(mismatchedPin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertText(mismatchedPin) - assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinsDontMatch) - state.eventSink(CreatePinEvents.ClearFailure) + assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.PinsDontMatch) + state.eventSink(SetupPinEvents.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isFalse() - assertThat(state.createPinFailure).isNull() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + assertThat(state.setupPinFailure).isNull() + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitItem().also { state -> state.choosePinEntry.assertText(completePin) @@ -108,7 +108,7 @@ class CreatePinPresenterTest { assertThat(isEmpty).isTrue() } - private fun createCreatePinPresenter(): CreatePinPresenter { - return CreatePinPresenter(PinValidator(), aBuildMeta()) + private fun createSetupPinPresenter(): SetupPinPresenter { + return SetupPinPresenter(PinValidator(), aBuildMeta()) } } From 35dce75fd1b2bad6eeba00edcedecaae234ea21b Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 10:34:49 +0200 Subject: [PATCH 116/281] PIN : fix warning --- .../features/lockscreen/impl/components/PinEntryTextField.kt | 2 +- .../android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index 5682e594fe..e4b6e6ac47 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -106,7 +106,7 @@ private fun PinDigitView( @PreviewsDayNight @Composable -fun PinEntryTextFieldPreview() { +internal fun PinEntryTextFieldPreview() { ElementTheme { PinEntryTextField( pinEntry = PinEntry.empty(4).fillWith("12"), diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt index 8a6230c725..46b9ca1b82 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt @@ -199,7 +199,7 @@ private fun PinKeypadBackButton( @Composable @PreviewsDayNight -fun PinKeypadPreview() { +internal fun PinKeypadPreview() { ElementPreview { BoxWithConstraints { PinKeypad( From 4fbe32a6daaf9419c95d98687ad9cca164ba075a Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 11:00:47 +0200 Subject: [PATCH 117/281] PIN: fix konsist --- .../android/features/lockscreen/impl/unlock/PinUnlockView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index be43d6cf32..e2cf522a07 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -126,7 +126,7 @@ fun PinUnlockView( } @Composable -fun PinUnlockCompactView( +private fun PinUnlockCompactView( header: @Composable () -> Unit, footer: @Composable () -> Unit, modifier: Modifier = Modifier, @@ -150,7 +150,7 @@ fun PinUnlockCompactView( } @Composable -fun PinUnlockExpandedView( +private fun PinUnlockExpandedView( header: @Composable () -> Unit, footer: @Composable () -> Unit, modifier: Modifier = Modifier, From 58f9c35ad8ccb75ab500b96979e7280941fa9f1a Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 23 Oct 2023 10:28:04 +0100 Subject: [PATCH 118/281] fix unused import --- .../test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt | 1 - 1 file changed, 1 deletion(-) 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 c05ddb6c73..fd5de85b1d 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -31,7 +31,6 @@ import io.element.android.appnav.room.RoomLoadedFlowNode import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.libraries.architecture.childNode -import io.element.android.libraries.architecture.createNode 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.room.FakeMatrixRoom From 6230d9d48b8c1638e5c46addb56bf5d1d108c1a8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 11:41:08 +0200 Subject: [PATCH 119/281] PIN : add test for SetupPinPresenter --- build.gradle.kts | 2 - features/lockscreen/impl/build.gradle.kts | 2 + .../lockscreen/impl/unlock/PinUnlockEvents.kt | 2 +- .../impl/unlock/PinUnlockPresenter.kt | 2 +- .../impl/pin/model/PinEntryAssertions.kt | 28 ++++++ .../impl/setup/SetupPinPresenterTest.kt | 11 +-- .../impl/unlock/PinUnlockPresenterTest.kt | 89 +++++++++++++++++++ 7 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 391313fe2d..f08c023b1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -250,8 +250,6 @@ koverMerged { excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" - // Temporary until we have actually something to test. - excludes += "io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter$*" } bound { minValue = 85 diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 8daeb2178c..a3f5b27949 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -51,6 +51,8 @@ dependencies { testImplementation(projects.tests.testutils) testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) + testImplementation(projects.libraries.featureflag.test) + ksp(libs.showkase.processor) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index a90b6cb702..5560dedd24 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -20,6 +20,6 @@ import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel sealed interface PinUnlockEvents { data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents - data object Unlock : PinUnlockEvents data object OnForgetPin : PinUnlockEvents + data object ClearSignOutPrompt : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index a6f06158d4..783e03c92a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -53,7 +53,6 @@ class PinUnlockPresenter @Inject constructor( fun handleEvents(event: PinUnlockEvents) { when (event) { - PinUnlockEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } is PinUnlockEvents.OnPinKeypadPressed -> { pinEntry = pinEntry.process(event.pinKeypadModel) if (pinEntry.isComplete()) { @@ -61,6 +60,7 @@ class PinUnlockPresenter @Inject constructor( } } PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true + PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false } } return PinUnlockState( diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt new file mode 100644 index 0000000000..37d54677a1 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt @@ -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. + */ + +package io.element.android.features.lockscreen.impl.pin.model + +import com.google.common.truth.Truth.assertThat + +fun PinEntry.assertText(text: String) { + assertThat(toText()).isEqualTo(text) +} + +fun PinEntry.assertEmpty() { + val isEmpty = digits.all { it is PinDigit.Empty } + assertThat(isEmpty).isTrue() +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index d7529be243..5d9901a9f1 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -22,6 +22,8 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.features.lockscreen.impl.pin.model.assertEmpty +import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.matrix.test.core.aBuildMeta @@ -99,15 +101,6 @@ class SetupPinPresenterTest { } } - private fun PinEntry.assertText(text: String) { - assertThat(toText()).isEqualTo(text) - } - - private fun PinEntry.assertEmpty() { - val isEmpty = digits.all { it is PinDigit.Empty } - assertThat(isEmpty).isTrue() - } - private fun createSetupPinPresenter(): SetupPinPresenter { return SetupPinPresenter(PinValidator(), aBuildMeta()) } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt new file mode 100644 index 0000000000..6839fe65c7 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -0,0 +1,89 @@ +/* + * 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.lockscreen.impl.unlock + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.pin.model.assertEmpty +import io.element.android.features.lockscreen.impl.pin.model.assertText +import io.element.android.features.lockscreen.impl.state.DefaultLockScreenStateService +import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PinUnlockPresenterTest { + + private val halfCompletePin = "12" + private val completePin = "1235" + + @Test + fun `present - complete flow`() = runTest { + val presenter = createPinUnlockPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().also { state -> + state.pinEntry.assertEmpty() + assertThat(state.showWrongPinTitle).isFalse() + assertThat(state.showSignOutPrompt).isFalse() + assertThat(state.remainingAttempts).isEqualTo(3) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(halfCompletePin) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty)) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(halfCompletePin) + state.eventSink(PinUnlockEvents.OnForgetPin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showSignOutPrompt).isEqualTo(true) + assertThat(state.isSignOutPromptCancellable).isEqualTo(true) + state.eventSink(PinUnlockEvents.ClearSignOutPrompt) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showSignOutPrompt).isEqualTo(false) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(completePin) + } + } + } + + private suspend fun createPinUnlockPresenter(scope: CoroutineScope): PinUnlockPresenter { + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.PinUnlock, true) + } + val lockScreenStateService = DefaultLockScreenStateService(featureFlagService) + return PinUnlockPresenter( + lockScreenStateService, + scope, + ) + } +} From 2652723c58f473572f63bfbf6598d5f006a4853a Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 12:12:53 +0200 Subject: [PATCH 120/281] Lock settings : create UI --- .../impl/settings/LockScreenSettingsEvents.kt | 25 ++++++ .../impl/settings/LockScreenSettingsNode.kt | 44 ++++++++++ .../settings/LockScreenSettingsPresenter.kt | 59 +++++++++++++ .../impl/settings/LockScreenSettingsState.kt | 24 ++++++ .../LockScreenSettingsStateProvider.kt | 39 +++++++++ .../impl/settings/LockScreenSettingsView.kt | 85 +++++++++++++++++++ 6 files changed, 276 insertions(+) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt new file mode 100644 index 0000000000..005077d81b --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.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.features.lockscreen.impl.settings + +sealed interface LockScreenSettingsEvents { + data object RemovePin : LockScreenSettingsEvents + data object ConfirmRemovePin : LockScreenSettingsEvents + data object CancelRemovePin : LockScreenSettingsEvents + data object ChangePin : LockScreenSettingsEvents + data object ToggleBiometric : LockScreenSettingsEvents +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt new file mode 100644 index 0000000000..6b47984076 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt @@ -0,0 +1,44 @@ +/* + * 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.lockscreen.impl.settings + +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.AppScope + +@ContributesNode(AppScope::class) +class LockScreenSettingsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: LockScreenSettingsPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + LockScreenSettingsView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt new file mode 100644 index 0000000000..2f02bb3982 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -0,0 +1,59 @@ +/* + * 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.lockscreen.impl.settings + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class LockScreenSettingsPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): LockScreenSettingsState { + + var isLockMandatory by remember { + mutableStateOf(false) + } + var isBiometricEnabled by remember { + mutableStateOf(false) + } + var showRemovePinConfirmation by remember { + mutableStateOf(false) + } + + fun handleEvents(event: LockScreenSettingsEvents) { + when (event) { + LockScreenSettingsEvents.CancelRemovePin -> TODO() + LockScreenSettingsEvents.ChangePin -> TODO() + LockScreenSettingsEvents.ConfirmRemovePin -> TODO() + LockScreenSettingsEvents.RemovePin -> TODO() + LockScreenSettingsEvents.ToggleBiometric -> TODO() + } + } + + return LockScreenSettingsState( + isLockMandatory = isLockMandatory, + isBiometricEnabled = isBiometricEnabled, + showRemovePinConfirmation = showRemovePinConfirmation, + eventSink = ::handleEvents + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt new file mode 100644 index 0000000000..06f74af9c9 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt @@ -0,0 +1,24 @@ +/* + * 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.lockscreen.impl.settings + +data class LockScreenSettingsState( + val isLockMandatory: Boolean, + val isBiometricEnabled: Boolean, + val showRemovePinConfirmation: Boolean, + val eventSink: (LockScreenSettingsEvents) -> Unit +) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt new file mode 100644 index 0000000000..3c4ce83452 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt @@ -0,0 +1,39 @@ +/* + * 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.lockscreen.impl.settings + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class LockScreenSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLockScreenSettingsState(), + aLockScreenSettingsState(isLockMandatory = true), + aLockScreenSettingsState(showRemovePinConfirmation = true), + ) +} + +fun aLockScreenSettingsState( + isLockMandatory: Boolean = false, + isBiometricEnabled: Boolean = false, + showRemovePinConfirmation: Boolean = false, +) = LockScreenSettingsState( + isLockMandatory = isLockMandatory, + isBiometricEnabled = isBiometricEnabled, + showRemovePinConfirmation = showRemovePinConfirmation, + eventSink = {} +) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt new file mode 100644 index 0000000000..57c07213ce --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -0,0 +1,85 @@ +/* + * 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.lockscreen.impl.settings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.lockscreen.impl.R +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider +import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch +import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun LockScreenSettingsView( + state: LockScreenSettingsState, + modifier: Modifier = Modifier, +) { + PreferencePage( + title = stringResource(id = io.element.android.libraries.ui.strings.R.string.common_screen_lock), + modifier = modifier + ) { + PreferenceCategory(showDivider = false) { + PreferenceText( + title = stringResource(id = R.string.screen_app_lock_settings_change_pin), + onClick = { + state.eventSink(LockScreenSettingsEvents.ChangePin) + } + ) + PreferenceDivider() + if (!state.isLockMandatory) { + PreferenceText( + title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), + tintColor = ElementTheme.colors.textCriticalPrimary, + onClick = { + state.eventSink(LockScreenSettingsEvents.RemovePin) + } + ) + } + PreferenceDivider() + PreferenceSwitch(title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock), isChecked = state.isBiometricEnabled) + } + } + if (state.showRemovePinConfirmation) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_title), + content = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_message), + onSubmitClicked = { + state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) + }, + onDismiss = { + state.eventSink(LockScreenSettingsEvents.CancelRemovePin) + }) + } +} + +@PreviewsDayNight +@Composable +internal fun LockScreenSettingsViewPreview( + @PreviewParameter(LockScreenSettingsStateProvider::class) state: LockScreenSettingsState, +) { + ElementPreview { + LockScreenSettingsView(state) + } +} From 8ca42d4cfcff383690e47e7911a2d8af06727f0a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 23 Oct 2023 11:57:04 +0200 Subject: [PATCH 121/281] Konsist: add test to ensure that functions with `@PreviewsDayNight` contain `ElementTheme` composable, and fix existing issues. --- .../lockscreen/impl/components/PinEntryTextField.kt | 3 ++- .../virtual/TimelineEncryptedHistoryBannerView.kt | 3 ++- .../libraries/textcomposer/TextComposerLinkDialog.kt | 7 ++++--- .../textcomposer/components/RecordingProgress.kt | 7 +++---- .../android/tests/konsist/KonsistPreviewTest.kt | 11 +++++++++++ 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index e4b6e6ac47..fceaf59f2a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.pinDigitBg import io.element.android.libraries.theme.ElementTheme @@ -107,7 +108,7 @@ private fun PinDigitView( @PreviewsDayNight @Composable internal fun PinEntryTextFieldPreview() { - ElementTheme { + ElementPreview { PinEntryTextField( pinEntry = PinEntry.empty(4).fillWith("12"), onValueChange = {}, 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 0317a68a00..2ef6c6580c 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 @@ -29,6 +29,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.R +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.utils.CommonDrawables @@ -61,7 +62,7 @@ fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) { @PreviewsDayNight @Composable internal fun TimelineEncryptedHistoryBannerViewPreview() { - ElementTheme { + ElementPreview { TimelineEncryptedHistoryBannerView() } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt index f36e2ca11c..232e9ab0c1 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import io.element.android.libraries.designsystem.components.dialogs.ListDialog import io.element.android.libraries.designsystem.components.list.TextFieldListItem +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text @@ -200,7 +201,7 @@ private fun EditLinkDialog( @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogCreateLinkPreview() { +internal fun TextComposerLinkDialogCreateLinkPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.InsertLink, @@ -212,7 +213,7 @@ internal fun TextComposerLinkDialogCreateLinkPreview() { @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() { +internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.SetLink(null), @@ -224,7 +225,7 @@ internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() { @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogEditLinkPreview() { +internal fun TextComposerLinkDialogEditLinkPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.SetLink("https://element.io"), diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt index db4f59342c..2fc0420e05 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +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 import io.element.android.libraries.theme.ElementTheme @@ -46,9 +47,7 @@ internal fun RecordingProgress( shape = MaterialTheme.shapes.medium, ) .padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) - .heightIn(26.dp) - - , + .heightIn(26.dp), verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -69,6 +68,6 @@ internal fun RecordingProgress( @PreviewsDayNight @Composable -internal fun RecordingProgressPreview() { +internal fun RecordingProgressPreview() = ElementPreview { RecordingProgress() } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 40c73c8eaa..7061d4ba55 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -35,4 +35,15 @@ class KonsistPreviewTest { it.hasNameEndingWith("DarkPreview").not() } } + + @Test + fun `Functions with '@PreviewsDayNight' annotation should contain 'ElementPreview' composable`() { + Konsist + .scopeFromProject() + .functions() + .withAllAnnotationsOf(PreviewsDayNight::class) + .assertTrue { + it.text.contains("ElementPreview") + } + } } From 6a3c42f189acf7a9695f34404587e13920914ea2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 23 Oct 2023 12:00:01 +0200 Subject: [PATCH 122/281] Konsist: add test to ensure that functions with `@PreviewsDayNight` are internal, and fix existing issues. --- .../android/tests/konsist/KonsistPreviewTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 7061d4ba55..f82f4dac7c 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -46,4 +46,15 @@ class KonsistPreviewTest { it.text.contains("ElementPreview") } } + + @Test + fun `Functions with '@PreviewsDayNight' are internal`() { + Konsist + .scopeFromProject() + .functions() + .withAllAnnotationsOf(PreviewsDayNight::class) + .assertTrue { + it.hasInternalModifier + } + } } From afc3958ecfda9b7c871c825397f1be027c12523a Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 23 Oct 2023 12:06:52 +0100 Subject: [PATCH 123/281] Fix emoji shortcut values --- .../android/features/messages/impl/actionlist/ActionListView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 05db97d3be..34d87201a2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -348,7 +348,7 @@ private fun EmojiReactionsRow( ) { // TODO use most recently used emojis here when available from the Rust SDK val defaultEmojis = sequenceOf( - "👍", "👎", "🔥", "❤️", "👏" + "👍️", "👎️", "🔥", "❤️", "👏" ) for (emoji in defaultEmojis) { val isHighlighted = highlightedEmojis.contains(emoji) From f8512d3964bb89c8d94e78c9469f5c921b90ed6c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 23 Oct 2023 14:01:23 +0200 Subject: [PATCH 124/281] Remove unused import --- .../features/lockscreen/impl/setup/SetupPinPresenterTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 5d9901a9f1..94877fbff5 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,8 +20,6 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.pin.model.PinDigit -import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator From 6478166263b028be9ff1a8ea17286516ccc37297 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 23 Oct 2023 12:11:23 +0000 Subject: [PATCH 125/281] Update screenshots --- ...ionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png index ac13aa8b52..385f874f07 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e7684929f93f437c50549b309e1ba75b95cddb506ad9c8ef97373d0552bddfb -size 39017 +oid sha256:1997fc442ccd0abe76d0fdcde639e8224e339f8b93cc3703543b39ec641edcf8 +size 37486 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png index 18aaab9fd1..44fb2cdf99 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56f71535fd676a86e79b0e987701d34c85c96276e5338f77117b13e1b3a05144 -size 45715 +oid sha256:72976810fdc5da4e2182db9ddcd1972a3d5f58510a2083a58513441cce02579e +size 43960 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png index 9098eef49b..cbf0e90093 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5554661aeb29ec0d95464942da03df5e83dc78762bc08e2f57851a8f7e811a5a -size 39438 +oid sha256:fc9f27f9d0068e1010eccbc776af4106bdb95811f6c3bdd3d13de26eea254b22 +size 37902 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png index 0c6cb3114f..483ae0043a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d398070330d5c66358b7ac17d4e1b93795fb9001ffc6eabe533d3bae86de7d58 -size 39810 +oid sha256:0e271ef60b51973bdb283f656f6c2bbfbb8bbfc723f15128e04e0ff1123dc763 +size 38256 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png index 74448d6967..d65f7aaa8a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:979138753000ada021a67f4bc14a89b912430c4331ac69aa702b2e50c050d7cb -size 41200 +oid sha256:207ee896047bed185f6110dd032924c9ed3ffa40a58ca6c2e829797e3074d460 +size 39600 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png index 2e051be73c..fd436c9971 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb4be22a56036d53f7c394c3829a9f8f09820276d7529ace0d3731965a8a92f9 -size 37556 +oid sha256:9a82276d1fa57bbed2850244886071df37b6441d7842acef4ecefd6a161404d7 +size 36106 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png index ea837fc372..125bcd6f79 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:369e8b4e3f741476cfa5ff7e3f9d5d1e1399fcf3494fadb95540129436ef4d68 -size 43744 +oid sha256:440aa47c7c00d1957a800b245de79b9b9b9a570de7024a0da211df681effc5f3 +size 42340 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png index 1c028e2b60..8aecf8543a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cd8fe02340aeb0edb116c0f3d8f07efc1d331849688718782db25e17bb6e1ab -size 37766 +oid sha256:6c595a14f273e1ffbda9ecbe499acccb49d4ee5ff32aaa3f8d5cfee0f7cf86d2 +size 36277 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png index cb01f9579d..23ebba41cf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b2aaa54bf065e6496957c3a60d61336f65e6b76f97e5dbc773284ee49fac91d -size 37988 +oid sha256:babf4bbf8494b67b000177fca06fb8817f35ba5b5851cd85e729ad1d70a9bd95 +size 36515 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png index 8d45efac8c..ac459d9d58 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752abe37a810cc8e4b36ae5f4c4aa38c9f22757b6458f272ba991050c69aee53 -size 39409 +oid sha256:1845e027e45d15aadc4be9c579568a791623adf25ff166ffb1595333b4535833 +size 37953 From 05a999acd4832c3cb1d62a852b49c483f595abb7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 15:30:17 +0200 Subject: [PATCH 126/281] Pin: add LockScreenConfig and address PR reviews --- .../android/appconfig/LockScreenConfig.kt | 35 +++++++++++++++++++ features/lockscreen/impl/build.gradle.kts | 5 ++- .../impl/components/PinEntryTextField.kt | 2 +- .../lockscreen/impl/pin/model/PinEntry.kt | 4 +-- .../impl/setup/SetupPinPresenter.kt | 15 ++++---- .../impl/setup/SetupPinStateProvider.kt | 14 ++++---- .../impl/setup/validation/PinValidator.kt | 9 ++--- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 2 +- .../impl/unlock/PinUnlockPresenter.kt | 8 +++-- .../impl/unlock/PinUnlockStateProvider.kt | 4 +-- .../lockscreen/impl/unlock/PinUnlockView.kt | 2 +- .../unlock/{numpad => keypad}/PinKeypad.kt | 2 +- .../{numpad => keypad}/PinKeypadModel.kt | 2 +- .../impl/setup/SetupPinPresenterTest.kt | 3 +- .../impl/unlock/PinUnlockPresenterTest.kt | 2 +- 15 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/{numpad => keypad}/PinKeypad.kt (99%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/{numpad => keypad}/PinKeypadModel.kt (92%) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt new file mode 100644 index 0000000000..5930f53428 --- /dev/null +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.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.appconfig + +object LockScreenConfig { + + /** + * Whether the LockScreen is mandatory or not. + */ + const val IS_MANDATORY: Boolean = false + + /** + * Some PINs are blacklisted. + */ + val PIN_BLACKLIST = listOf("0000", "1234") + + /** + * The size of the PIN. + */ + const val PIN_SIZE = 4 +} diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index a3f5b27949..a3657ccff3 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -30,9 +30,11 @@ anvil { } dependencies { + ksp(libs.showkase.processor) implementation(projects.anvilannotations) anvil(projects.anvilcodegen) api(projects.features.lockscreen.api) + implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -52,7 +54,4 @@ dependencies { testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) testImplementation(projects.libraries.featureflag.test) - - - ksp(libs.showkase.processor) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index fceaf59f2a..91f6d435c5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -110,7 +110,7 @@ private fun PinDigitView( internal fun PinEntryTextFieldPreview() { ElementPreview { PinEntryTextField( - pinEntry = PinEntry.empty(4).fillWith("12"), + pinEntry = PinEntry.createEmpty(4).fillWith("12"), onValueChange = {}, ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index 92dda869a6..eaca592de9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -24,7 +24,7 @@ data class PinEntry( ) { companion object { - fun empty(size: Int): PinEntry { + fun createEmpty(size: Int): PinEntry { val digits = List(size) { PinDigit.Empty } return PinEntry( digits = digits.toPersistentList() @@ -69,7 +69,7 @@ data class PinEntry( } fun clear(): PinEntry { - return fillWith("") + return createEmpty(size) } fun isComplete(): Boolean { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 6a880a6967..3c380e6be7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -21,15 +21,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.features.lockscreen.impl.setup.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import javax.inject.Inject -private const val PIN_SIZE = 4 - class SetupPinPresenter @Inject constructor( private val pinValidator: PinValidator, private val buildMeta: BuildMeta, @@ -38,10 +37,10 @@ class SetupPinPresenter @Inject constructor( @Composable override fun present(): SetupPinState { var choosePinEntry by remember { - mutableStateOf(PinEntry.empty(PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } var confirmPinEntry by remember { - mutableStateOf(PinEntry.empty(PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } var isConfirmationStep by remember { mutableStateOf(false) @@ -77,11 +76,11 @@ class SetupPinPresenter @Inject constructor( SetupPinEvents.ClearFailure -> { when (setupPinFailure) { is SetupPinFailure.PinsDontMatch -> { - choosePinEntry = PinEntry.empty(PIN_SIZE) - confirmPinEntry = PinEntry.empty(PIN_SIZE) + choosePinEntry = choosePinEntry.clear() + confirmPinEntry = confirmPinEntry.clear() } is SetupPinFailure.PinBlacklisted -> { - choosePinEntry = PinEntry.empty(PIN_SIZE) + choosePinEntry = choosePinEntry.clear() } null -> Unit } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt index 1a177b4a83..bb0a46d10c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -25,20 +25,20 @@ open class SetupPinStateProvider : PreviewParameterProvider { get() = sequenceOf( aSetupPinState(), aSetupPinState( - choosePinEntry = PinEntry.empty(4).fillWith("12") + choosePinEntry = PinEntry.createEmpty(4).fillWith("12") ), aSetupPinState( - choosePinEntry = PinEntry.empty(4).fillWith("1789"), + choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"), isConfirmationStep = true, ), aSetupPinState( - choosePinEntry = PinEntry.empty(4).fillWith("1789"), - confirmPinEntry = PinEntry.empty(4).fillWith("1788"), + choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"), + confirmPinEntry = PinEntry.createEmpty(4).fillWith("1788"), isConfirmationStep = true, creationFailure = SetupPinFailure.PinsDontMatch ), aSetupPinState( - choosePinEntry = PinEntry.empty(4).fillWith("1111"), + choosePinEntry = PinEntry.createEmpty(4).fillWith("1111"), creationFailure = SetupPinFailure.PinBlacklisted ), @@ -46,8 +46,8 @@ open class SetupPinStateProvider : PreviewParameterProvider { } fun aSetupPinState( - choosePinEntry: PinEntry = PinEntry.empty(4), - confirmPinEntry: PinEntry = PinEntry.empty(4), + choosePinEntry: PinEntry = PinEntry.createEmpty(4), + confirmPinEntry: PinEntry = PinEntry.createEmpty(4), isConfirmationStep: Boolean = false, creationFailure: SetupPinFailure? = null, ) = SetupPinState( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index c7435120aa..20ad023b1c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -16,17 +16,12 @@ package io.element.android.features.lockscreen.impl.setup.validation -import androidx.annotation.VisibleForTesting +import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject class PinValidator @Inject constructor() { - companion object { - @VisibleForTesting - val BLACKLIST = listOf("0000", "1234") - } - sealed interface Result { data object Valid : Result data class Invalid(val failure: SetupPinFailure) : Result @@ -34,7 +29,7 @@ class PinValidator @Inject constructor() { fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() - val isBlacklisted = BLACKLIST.any { it == pinAsText } + val isBlacklisted = LockScreenConfig.PIN_BLACKLIST.any { it == pinAsText } return if (isBlacklisted) { Result.Invalid(SetupPinFailure.PinBlacklisted) } else { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt index 5560dedd24..30ee16df02 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -16,7 +16,7 @@ package io.element.android.features.lockscreen.impl.unlock -import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel sealed interface PinUnlockEvents { data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 783e03c92a..e189a2ab39 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -23,9 +23,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -39,9 +40,11 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { var pinEntry by remember { - mutableStateOf(PinEntry.empty(4)) + //TODO fetch size from db + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } var remainingAttempts by rememberSaveable { + //TODO fetch from db mutableIntStateOf(3) } var showWrongPinTitle by rememberSaveable { @@ -56,6 +59,7 @@ class PinUnlockPresenter @Inject constructor( is PinUnlockEvents.OnPinKeypadPressed -> { pinEntry = pinEntry.process(event.pinKeypadModel) if (pinEntry.isComplete()) { + //TODO check pin with PinCodeManager coroutineScope.launch { pinStateService.unlock() } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 4f269d2f5a..8ddc942e25 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -23,7 +23,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPinUnlockState(), - aPinUnlockState(pinEntry = PinEntry.empty(4).fillWith("12")), + aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")), aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), @@ -31,7 +31,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider { } fun aPinUnlockState( - pinEntry: PinEntry = PinEntry.empty(4), + pinEntry: PinEntry = PinEntry.createEmpty(4), remainingAttempts: Int = 3, showWrongPinTitle: Boolean = false, showSignOutPrompt: Boolean = false, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index e2cf522a07..37cb591007 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.pin.model.PinDigit import io.element.android.features.lockscreen.impl.pin.model.PinEntry -import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypad +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt similarity index 99% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt index 46b9ca1b82..1056718dd0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.unlock.numpad +package io.element.android.features.lockscreen.impl.unlock.keypad import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt similarity index 92% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt index f1430dcaa5..8d232cb21b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/numpad/PinKeypadModel.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.unlock.numpad +package io.element.android.features.lockscreen.impl.unlock.keypad import androidx.compose.runtime.Immutable diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 94877fbff5..21005cc722 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,6 +20,7 @@ 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.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator @@ -31,7 +32,7 @@ import org.junit.Test class SetupPinPresenterTest { - private val blacklistedPin = PinValidator.BLACKLIST.first() + private val blacklistedPin = LockScreenConfig.PIN_BLACKLIST private val halfCompletePin = "12" private val completePin = "1235" private val mismatchedPin = "1236" diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index 6839fe65c7..02919edce0 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.state.DefaultLockScreenStateService -import io.element.android.features.lockscreen.impl.unlock.numpad.PinKeypadModel +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.tests.testutils.awaitLastSequentialItem From da4dd44654c0209d8635be4d6d5a7f2687de1b36 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 15:38:24 +0200 Subject: [PATCH 127/281] PIN unlock : adjust ui a bit --- .../android/features/lockscreen/impl/unlock/PinUnlockView.kt | 4 +++- .../features/lockscreen/impl/unlock/keypad/PinKeypad.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 37cb591007..5769f42b35 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -78,7 +78,9 @@ fun PinUnlockView( ) } val footer = @Composable { - PinUnlockFooter() + PinUnlockFooter( + modifier = Modifier.padding(top = 24.dp) + ) } val content = @Composable { constraints: BoxWithConstraintsScope -> PinKeypad( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt index 1056718dd0..9db5cfe11a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt @@ -49,7 +49,7 @@ import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -private val spaceBetweenPinKey = 8.dp +private val spaceBetweenPinKey = 16.dp private val maxSizePinKey = 80.dp @Composable From 4bcb17f09f46b5e6d34267443cf15e0bc0fe4006 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 23 Oct 2023 13:49:14 +0000 Subject: [PATCH 128/281] Update screenshots --- ...nlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png | 3 +++ ...nlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png | 3 +++ ...nlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png | 3 --- ...nlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png | 3 --- ...unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png | 4 ++-- 14 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dbb5213ce6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbb48906ee7f1f18034cd7c64766790689f95f4ea4c0310a98430921b3792aee +size 30854 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a018f0927 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ec32849337493132ab88ebc7270728e3c568896e40b0f451b633c924591cbaa +size 28579 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index fc54ab5f7e..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b14ff41b74e36809f359a7b3b73ffdb9c0c2cd97ec786333bcce9412f05825e6 -size 30608 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 62b7c73a26..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.numpad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1521d9fecaaadcfc7ed4670d997b36a20bd2a84f564794a06cb10bfe7d1ad64e -size 28812 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png index e81bbf1c1d..c508164493 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c369763c4be676d6727a6b0f5cd74d56a9de09b9c3780e1806874d19ba7ffd3f -size 42705 +oid sha256:c94f10441359bdb901ee1f8be9c307312586d6ca54495e88ec231c1e931eb36b +size 38994 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png index bcb90d6b4a..07c06a033c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a63439f320a1b6ad124cb943f95651f85d2e9cfc34597cb450c803b6dafaefa7 -size 43149 +oid sha256:38456a511fbc72e3d544c8f88138be4f24b6adb3a10629989c037141213a9e19 +size 39441 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png index bf2823f6a4..4a8df974cd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:165e37ef78a7b09bbd49e8d692ccb27836e35d8535b182d9837a5c3daa5cf3a6 -size 43978 +oid sha256:083c3972e5d019ce1d8565a655bd6f6fb6689e17792ddf7ed282d5eee529a7b6 +size 40292 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png index b3ca08978d..02570ec381 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ecaba43c72a890d85f9894eb987f291967c8ea15bdc09db88d6a72384098d827 -size 48308 +oid sha256:bda49a721fbd19b4cbe0f4dc1b4013154a55e17f78e099f411d1caaef82cfa6c +size 46711 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png index 5b8c62dcbf..ee9f61a453 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25175242be4133a74541f0bb2d05db95484a65da952031fb8f68a2fdef88a995 -size 45880 +oid sha256:28d2e1634cc0bbf01f6efc2b260625abb26f2105bf1e858f2bd196830d70854c +size 44278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png index fc34314f30..c7864da5cc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:699c96cc0d06c85eeda3a3b408877d37445bebf1873a86d6255ba8626ba410b0 -size 39654 +oid sha256:ab8c04689414b5183fa6ab5a479063b478b3d02dfb6bf2df72b1b606c77392ad +size 36278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png index 6d029a3034..5d49a0e78a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:233bb8cb261f752ac868c492d075fea9e5af656fcc8ad0d12fbf05b2dd690788 -size 40113 +oid sha256:fda961a2240c1458ad561c8f4e7fa265bbe6c70cf9b0fbfcd1ae0879036a3343 +size 36714 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png index af181f44e5..b386f26669 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:920b9f28c3d2f5e726ad4beeb668e75c285b0e731e6d7eaaf87ca3b2a8b9980b -size 40652 +oid sha256:0cb889338b21dd8e1a913348ff02cd217f48840c4d54b99ed4d89b67dff771bb +size 37254 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png index 2616989dcd..39854067a2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28197831dacc61f25d6b3a0080db331ef317283e8032fa5cc4ed457709da0f89 -size 42980 +oid sha256:7b0011f773767d2806b2269281415f910b4770bc0e95dff2a2a29a2c46f9e458 +size 41488 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png index ce7df11d83..bb647daed5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:652154d82c5f43f91cface3f2e5650ee5a98cf1faed08a80589ab42225bd8120 -size 40557 +oid sha256:d50c34cdf50881d8176d90617c717f7214faeaa4fdce22ec4b93b0e5669b9869 +size 39073 From 20eed4f7d7c7dcd83d92f5e87dba3311e6310919 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 16:03:11 +0200 Subject: [PATCH 129/281] PIN : fix tests with new LockScreenConfig --- .../io/element/android/appconfig/LockScreenConfig.kt | 6 +++--- .../lockscreen/impl/setup/validation/PinValidator.kt | 7 +++++-- .../lockscreen/impl/setup/SetupPinPresenterTest.kt | 5 ++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index 5930f53428..9427a1f9c7 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -19,14 +19,14 @@ package io.element.android.appconfig object LockScreenConfig { /** - * Whether the LockScreen is mandatory or not. + * Whether the PIN is mandatory or not. */ - const val IS_MANDATORY: Boolean = false + const val IS_PIN_MANDATORY: Boolean = false /** * Some PINs are blacklisted. */ - val PIN_BLACKLIST = listOf("0000", "1234") + val PIN_BLACKLIST = setOf("0000", "1234") /** * The size of the PIN. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index 20ad023b1c..b164ee8c88 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -20,7 +20,10 @@ import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject -class PinValidator @Inject constructor() { +class PinValidator internal constructor(private val pinBlacklist: Set) { + + @Inject + constructor() : this(LockScreenConfig.PIN_BLACKLIST) sealed interface Result { data object Valid : Result @@ -29,7 +32,7 @@ class PinValidator @Inject constructor() { fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() - val isBlacklisted = LockScreenConfig.PIN_BLACKLIST.any { it == pinAsText } + val isBlacklisted = pinBlacklist.any { it == pinAsText } return if (isBlacklisted) { Result.Invalid(SetupPinFailure.PinBlacklisted) } else { diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 21005cc722..ff797b52f4 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,7 +20,6 @@ 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.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator @@ -32,7 +31,7 @@ import org.junit.Test class SetupPinPresenterTest { - private val blacklistedPin = LockScreenConfig.PIN_BLACKLIST + private val blacklistedPin = "1234" private val halfCompletePin = "12" private val completePin = "1235" private val mismatchedPin = "1236" @@ -101,6 +100,6 @@ class SetupPinPresenterTest { } private fun createSetupPinPresenter(): SetupPinPresenter { - return SetupPinPresenter(PinValidator(), aBuildMeta()) + return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta()) } } From f159ca9d7dfa503f82d9ca8f39f1301372bee03a Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 23 Oct 2023 15:25:28 +0100 Subject: [PATCH 130/281] Update thumbs up emoji in the state provider. --- .../features/messages/impl/timeline/TimelineStateProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1374f4aef4..2955c6783f 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 @@ -139,7 +139,7 @@ fun aTimelineItemReactions( count: Int = 1, isHighlighted: Boolean = false, ): TimelineItemReactions { - val emojis = arrayOf("👍", "😀️", "😁️", "😆️", "😅️", "🤣️", "🥰️", "😇️", "😊️", "😉️", "🙃️", "🙂️", "😍️", "🤗️", "🤭️") + val emojis = arrayOf("👍️", "😀️", "😁️", "😆️", "😅️", "🤣️", "🥰️", "😇️", "😊️", "😉️", "🙃️", "🙂️", "😍️", "🤗️", "🤭️") return TimelineItemReactions( reactions = buildList { repeat(count) { index -> From 7e6cc4db9c48b5b1eff01302fc89310957102853 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 23 Oct 2023 14:39:14 +0000 Subject: [PATCH 131/281] Update screenshots --- ...ionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png index 385f874f07..ac13aa8b52 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1997fc442ccd0abe76d0fdcde639e8224e339f8b93cc3703543b39ec641edcf8 -size 37486 +oid sha256:2e7684929f93f437c50549b309e1ba75b95cddb506ad9c8ef97373d0552bddfb +size 39017 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png index 44fb2cdf99..18aaab9fd1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72976810fdc5da4e2182db9ddcd1972a3d5f58510a2083a58513441cce02579e -size 43960 +oid sha256:56f71535fd676a86e79b0e987701d34c85c96276e5338f77117b13e1b3a05144 +size 45715 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png index cbf0e90093..9098eef49b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc9f27f9d0068e1010eccbc776af4106bdb95811f6c3bdd3d13de26eea254b22 -size 37902 +oid sha256:5554661aeb29ec0d95464942da03df5e83dc78762bc08e2f57851a8f7e811a5a +size 39438 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png index 483ae0043a..0c6cb3114f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e271ef60b51973bdb283f656f6c2bbfbb8bbfc723f15128e04e0ff1123dc763 -size 38256 +oid sha256:d398070330d5c66358b7ac17d4e1b93795fb9001ffc6eabe533d3bae86de7d58 +size 39810 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png index d65f7aaa8a..74448d6967 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:207ee896047bed185f6110dd032924c9ed3ffa40a58ca6c2e829797e3074d460 -size 39600 +oid sha256:979138753000ada021a67f4bc14a89b912430c4331ac69aa702b2e50c050d7cb +size 41200 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png index fd436c9971..2e051be73c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a82276d1fa57bbed2850244886071df37b6441d7842acef4ecefd6a161404d7 -size 36106 +oid sha256:cb4be22a56036d53f7c394c3829a9f8f09820276d7529ace0d3731965a8a92f9 +size 37556 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png index 125bcd6f79..ea837fc372 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:440aa47c7c00d1957a800b245de79b9b9b9a570de7024a0da211df681effc5f3 -size 42340 +oid sha256:369e8b4e3f741476cfa5ff7e3f9d5d1e1399fcf3494fadb95540129436ef4d68 +size 43744 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png index 8aecf8543a..1c028e2b60 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c595a14f273e1ffbda9ecbe499acccb49d4ee5ff32aaa3f8d5cfee0f7cf86d2 -size 36277 +oid sha256:5cd8fe02340aeb0edb116c0f3d8f07efc1d331849688718782db25e17bb6e1ab +size 37766 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png index 23ebba41cf..cb01f9579d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:babf4bbf8494b67b000177fca06fb8817f35ba5b5851cd85e729ad1d70a9bd95 -size 36515 +oid sha256:9b2aaa54bf065e6496957c3a60d61336f65e6b76f97e5dbc773284ee49fac91d +size 37988 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png index ac459d9d58..8d45efac8c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1845e027e45d15aadc4be9c579568a791623adf25ff166ffb1595333b4535833 -size 37953 +oid sha256:752abe37a810cc8e4b36ae5f4c4aa38c9f22757b6458f272ba991050c69aee53 +size 39409 From 587cfcd886ecc6e62a1eaf80f13fd891c356c674 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 18:11:02 +0200 Subject: [PATCH 132/281] Lock settings : branch the flow --- .../android/appnav/LoggedInFlowNode.kt | 8 ++++--- .../lockscreen/api/LockScreenEntryPoint.kt | 20 ++++++++++++++-- .../impl/DefaultLockScreenEntryPoint.kt | 23 +++++++++++++++++-- .../lockscreen/impl/LockScreenFlowNode.kt | 14 ++++++++++- .../settings/LockScreenSettingsPresenter.kt | 6 ++--- .../impl/settings/LockScreenSettingsState.kt | 2 +- .../LockScreenSettingsStateProvider.kt | 2 +- .../impl/settings/LockScreenSettingsView.kt | 2 +- features/preferences/impl/build.gradle.kts | 1 + .../preferences/impl/PreferencesFlowNode.kt | 14 +++++++++++ .../impl/root/PreferencesRootNode.kt | 6 +++++ .../impl/root/PreferencesRootPresenter.kt | 5 ++++ .../impl/root/PreferencesRootState.kt | 1 + .../impl/root/PreferencesRootStateProvider.kt | 1 + .../impl/root/PreferencesRootView.kt | 10 ++++++++ 15 files changed, 100 insertions(+), 15 deletions(-) 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 2f7eabb582..754ab167d1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -48,11 +48,11 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.invitelist.api.InviteListEntryPoint -import io.element.android.features.networkmonitor.api.NetworkMonitor -import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenState import io.element.android.features.lockscreen.api.LockScreenStateService +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.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -218,7 +218,9 @@ class LoggedInFlowNode @AssistedInject constructor( createNode(buildContext) } NavTarget.LockPermanent -> { - lockScreenEntryPoint.createNode(this, buildContext) + lockScreenEntryPoint.nodeBuilder(this, buildContext) + .target(LockScreenEntryPoint.Target.Unlock) + .build() } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt index 3c9aceb2c8..fce3706dce 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt @@ -16,6 +16,22 @@ package io.element.android.features.lockscreen.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint -interface LockScreenEntryPoint : SimpleFeatureEntryPoint +interface LockScreenEntryPoint : FeatureEntryPoint { + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun target(target: Target): NodeBuilder + fun build(): Node + } + + enum class Target { + Settings, + Setup, + Unlock + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 736be374cd..289f11113a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt @@ -27,7 +27,26 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder { + + var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock + + return object : LockScreenEntryPoint.NodeBuilder { + override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder { + innerTarget = target + return this + } + + override fun build(): Node { + val inputs = LockScreenFlowNode.Inputs( + when (innerTarget) { + LockScreenEntryPoint.Target.Unlock -> LockScreenFlowNode.NavTarget.Unlock + LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup + LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings + } + ) + return parentNode.createNode(buildContext, listOf(inputs)) + } + } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index fa3b88e18a..962a133424 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -27,9 +27,11 @@ import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsNode import io.element.android.features.lockscreen.impl.setup.SetupPinNode import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope @@ -41,19 +43,26 @@ class LockScreenFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Unlock, + initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, plugins = plugins, ) { + data class Inputs( + val initialNavTarget: NavTarget = NavTarget.Unlock, + ) : NodeInputs + sealed interface NavTarget : Parcelable { @Parcelize data object Unlock : NavTarget @Parcelize data object Setup : NavTarget + + @Parcelize + data object Settings : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -64,6 +73,9 @@ class LockScreenFlowNode @AssistedInject constructor( NavTarget.Setup -> { createNode(buildContext) } + NavTarget.Settings -> { + createNode(buildContext) + } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt index 2f02bb3982..4ac1d1c863 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import io.element.android.appconfig.LockScreenConfig import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -29,9 +30,6 @@ class LockScreenSettingsPresenter @Inject constructor() : Presenter Unit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt index 3c4ce83452..320ec1aa8b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt @@ -32,7 +32,7 @@ fun aLockScreenSettingsState( isBiometricEnabled: Boolean = false, showRemovePinConfirmation: Boolean = false, ) = LockScreenSettingsState( - isLockMandatory = isLockMandatory, + isPinMandatory = isLockMandatory, isBiometricEnabled = isBiometricEnabled, showRemovePinConfirmation = showRemovePinConfirmation, eventSink = {} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index 57c07213ce..55df5632f1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -48,7 +48,7 @@ fun LockScreenSettingsView( } ) PreferenceDivider() - if (!state.isLockMandatory) { + if (!state.isPinMandatory) { PreferenceText( title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), tintColor = ElementTheme.colors.textCriticalPrimary, diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 4fcc69ff6b..e4ec04c263 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) implementation(projects.features.rageshake.api) + implementation(projects.features.lockscreen.api) implementation(projects.features.analytics.api) implementation(projects.features.ftue.api) implementation(projects.features.logout.api) 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 6cf0390db2..5144cfeb3f 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 @@ -29,6 +29,7 @@ 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.lockscreen.api.LockScreenEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.impl.about.AboutNode import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode @@ -50,6 +51,7 @@ import kotlinx.parcelize.Parcelize class PreferencesFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val lockScreenEntryPoint: LockScreenEntryPoint, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -81,6 +83,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object NotificationSettings : NavTarget + @Parcelize + data object LockScreenSettings : NavTarget + @Parcelize data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget @@ -116,6 +121,10 @@ class PreferencesFlowNode @AssistedInject constructor( backstack.push(NavTarget.NotificationSettings) } + override fun onOpenLockScreenSettings() { + backstack.push(NavTarget.LockScreenSettings) + } + override fun onOpenAdvancedSettings() { backstack.push(NavTarget.AdvancedSettings) } @@ -162,6 +171,11 @@ class PreferencesFlowNode @AssistedInject constructor( val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser) createNode(buildContext, listOf(inputs)) } + NavTarget.LockScreenSettings -> { + lockScreenEntryPoint.nodeBuilder(this, buildContext) + .target(LockScreenEntryPoint.Target.Settings) + .build() + } } } 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 407832627b..7ea1fa2e8a 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 @@ -47,6 +47,7 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenAbout() fun onOpenDeveloperSettings() fun onOpenNotificationSettings() + fun onOpenLockScreenSettings() fun onOpenAdvancedSettings() fun onOpenUserProfile(matrixUser: MatrixUser) } @@ -93,6 +94,10 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenNotificationSettings() } } + private fun onOpenLockScreenSettings() { + plugins().forEach { it.onOpenLockScreenSettings() } + } + private fun onOpenUserProfile(matrixUser: MatrixUser) { plugins().forEach { it.onOpenUserProfile(matrixUser) } } @@ -115,6 +120,7 @@ class PreferencesRootNode @AssistedInject constructor( onSuccessLogout = { onSuccessLogout(activity, it) }, onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) }, onOpenNotificationSettings = this::onOpenNotificationSettings, + onOpenLockScreenSettings = this::onOpenLockScreenSettings, onOpenUserProfile = this::onOpenUserProfile, ) } 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 200785e03d..64132db982 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 @@ -68,6 +68,10 @@ class PreferencesRootPresenter @Inject constructor( LaunchedEffect(Unit) { showNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) } + val showLockScreenSettings = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + showLockScreenSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) + } // We should display the 'complete verification' option if the current session can be verified val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false) @@ -95,6 +99,7 @@ class PreferencesRootPresenter @Inject constructor( showAnalyticsSettings = hasAnalyticsProviders, showDeveloperSettings = showDeveloperSettings, showNotificationSettings = showNotificationSettings.value, + showLockScreenSettings = showLockScreenSettings.value, snackbarMessage = snackbarMessage, ) } 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 f61ea5890d..d6ff4855de 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 @@ -29,6 +29,7 @@ data class PreferencesRootState( val devicesManagementUrl: String?, val showAnalyticsSettings: Boolean, val showDeveloperSettings: Boolean, + val showLockScreenSettings: Boolean, val showNotificationSettings: Boolean, val snackbarMessage: SnackbarMessage?, ) 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 467ca4c6d6..07dd6240bf 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 @@ -30,5 +30,6 @@ fun aPreferencesRootState() = PreferencesRootState( showAnalyticsSettings = true, showDeveloperSettings = true, showNotificationSettings = true, + showLockScreenSettings = true, snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete), ) 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 2999ddea94..34a4890348 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 @@ -20,6 +20,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.outlined.InsertChart import androidx.compose.material.icons.outlined.VerifiedUser import androidx.compose.runtime.Composable @@ -53,6 +54,7 @@ fun PreferencesRootView( onManageAccountClicked: (url: String) -> Unit, onOpenAnalytics: () -> Unit, onOpenRageShake: () -> Unit, + onOpenLockScreenSettings: ()->Unit, onOpenAbout: () -> Unit, onOpenDeveloperSettings: () -> Unit, onOpenAdvancedSettings: () -> Unit, @@ -116,6 +118,13 @@ fun PreferencesRootView( iconResourceId = CommonDrawables.ic_compound_info, onClick = onOpenAbout, ) + if (state.showLockScreenSettings) { + PreferenceText( + title = stringResource(id = CommonStrings.common_screen_lock), + icon = Icons.Default.Lock, + onClick = onOpenLockScreenSettings, + ) + } HorizontalDivider() if (state.devicesManagementUrl != null) { PreferenceText( @@ -183,6 +192,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) { onSuccessLogout = {}, onManageAccountClicked = {}, onOpenNotificationSettings = {}, + onOpenLockScreenSettings = {}, onOpenUserProfile = {}, ) } From b476654489487da0b3543655945a256112ff58da Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 23 Oct 2023 18:28:00 +0100 Subject: [PATCH 133/281] Record and send voice messages (#1596) --------- Co-authored-by: ElementBot --- changelog.d/1596.feature | 1 + features/messages/impl/build.gradle.kts | 2 + .../impl/src/main/AndroidManifest.xml | 20 ++ .../messages/impl/MessagesPresenter.kt | 3 + .../features/messages/impl/MessagesState.kt | 1 + .../messages/impl/MessagesStateProvider.kt | 1 + .../features/messages/impl/MessagesView.kt | 19 + .../messagecomposer/MessageComposerView.kt | 9 +- .../VoiceMessageComposerEvents.kt | 5 + .../VoiceMessageComposerPresenter.kt | 156 ++++++++- .../VoiceMessageComposerState.kt | 1 + .../VoiceMessageComposerStateProvider.kt | 3 +- .../voicemessages/VoiceMessageException.kt | 26 ++ .../VoiceMessagePermissionRationaleDialog.kt | 37 ++ .../messages/MessagesPresenterTest.kt | 17 +- .../VoiceMessageComposerPresenterTest.kt | 327 +++++++++++++++++- gradle/libs.versions.toml | 2 + .../android/libraries/core/hash/Hash.kt | 35 ++ .../libraries/mediaupload/api/MediaSender.kt | 54 ++- .../mediaupload/api/MediaUploadInfo.kt | 1 + .../mediaupload/AndroidMediaPreProcessor.kt | 1 + .../mediaupload/test/FakeMediaPreProcessor.kt | 18 + .../api/PermissionsStateProvider.kt | 5 +- .../libraries/textcomposer/TextComposer.kt | 43 ++- ...dingProgress.kt => VoiceMessagePreview.kt} | 21 +- .../components/VoiceMessageRecording.kt | 102 ++++++ .../textcomposer/model/VoiceMessageState.kt | 6 +- libraries/voicerecorder/api/build.gradle.kts | 32 ++ .../voicerecorder/api/VoiceRecorder.kt | 55 +++ .../voicerecorder/api/VoiceRecorderState.kt | 44 +++ libraries/voicerecorder/impl/build.gradle.kts | 48 +++ .../voicerecorder/impl/VoiceRecorderImpl.kt | 132 +++++++ .../impl/audio/AndroidAudioReader.kt | 139 ++++++++ .../voicerecorder/impl/audio/Audio.kt | 28 ++ .../voicerecorder/impl/audio/AudioConfig.kt | 35 ++ .../impl/audio/AudioLevelCalculator.kt | 28 ++ .../voicerecorder/impl/audio/AudioReader.kt | 37 ++ .../impl/audio/DecibelAudioLevelCalculator.kt | 49 +++ .../impl/audio/DefaultEncoder.kt | 63 ++++ .../voicerecorder/impl/audio/Encoder.kt | 28 ++ .../voicerecorder/impl/audio/SampleRate.kt | 24 ++ .../impl/di/VoiceRecorderModule.kt | 58 ++++ .../impl/file/DefaultVoiceFileManager.kt | 49 +++ .../impl/file/VoiceFileConfig.kt | 30 ++ .../impl/file/VoiceFileManager.kt | 25 ++ .../impl/VoiceRecorderImplTest.kt | 134 +++++++ .../audio/DecibelAudioLevelCalculatorTest.kt | 46 +++ .../test/FakeAudioLevelCalculator.kt | 26 ++ .../voicerecorder/test/FakeAudioReader.kt | 49 +++ .../test/FakeAudioRecorderFactory.kt | 30 ++ .../voicerecorder/test/FakeEncoder.kt | 40 +++ .../voicerecorder/test/FakeFileSystem.kt | 43 +++ .../test/FakeVoiceFileManager.kt | 37 ++ libraries/voicerecorder/test/build.gradle.kts | 30 ++ .../voicerecorder/test/FakeVoiceRecorder.kt | 74 ++++ .../kotlin/extension/DependencyHandleScope.kt | 1 + ...ViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png | 4 +- ...ViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png | 4 +- ...gProgress-D-11_11_null,NEXUS_5,1.0,en].png | 3 - ...gProgress-N-11_12_null,NEXUS_5,1.0,en].png | 3 - ...ndButton-D-11_11_null,NEXUS_5,1.0,en].png} | 0 ...ndButton-N-11_12_null,NEXUS_5,1.0,en].png} | 0 ...rmatting-D-12_12_null,NEXUS_5,1.0,en].png} | 0 ...rmatting-N-12_13_null,NEXUS_5,1.0,en].png} | 0 ...gePreview-D-13_13_null,NEXUS_5,1.0,en].png | 3 + ...gePreview-N-13_14_null,NEXUS_5,1.0,en].png | 3 + ...Recording-D-14_14_null,NEXUS_5,1.0,en].png | 3 + ...Recording-N-14_15_null,NEXUS_5,1.0,en].png | 3 + 68 files changed, 2274 insertions(+), 82 deletions(-) create mode 100644 changelog.d/1596.feature create mode 100644 features/messages/impl/src/main/AndroidManifest.xml create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessagePermissionRationaleDialog.kt create mode 100644 libraries/core/src/main/kotlin/io/element/android/libraries/core/hash/Hash.kt rename libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/{RecordingProgress.kt => VoiceMessagePreview.kt} (75%) create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt create mode 100644 libraries/voicerecorder/api/build.gradle.kts create mode 100644 libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorder.kt create mode 100644 libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt create mode 100644 libraries/voicerecorder/impl/build.gradle.kts create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Audio.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioConfig.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioReader.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Encoder.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/SampleRate.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileConfig.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileManager.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioRecorderFactory.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeEncoder.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeFileSystem.kt create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceFileManager.kt create mode 100644 libraries/voicerecorder/test/build.gradle.kts create mode 100644 libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-D-11_11_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordingProgress-N-11_12_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_SendButton-D-11_11_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_SendButton-N-11_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_TextFormatting-D-12_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_TextFormatting-N-12_13_null,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-13_13_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-13_14_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-14_14_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-14_15_null,NEXUS_5,1.0,en].png diff --git a/changelog.d/1596.feature b/changelog.d/1596.feature new file mode 100644 index 0000000000..5108d6008b --- /dev/null +++ b/changelog.d/1596.feature @@ -0,0 +1 @@ +Record and send voice messages \ No newline at end of file diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 956b80949a..8ee3adb13c 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.voicerecorder.api) implementation(projects.features.networkmonitor.api) implementation(projects.services.analytics.api) implementation(libs.coil.compose) @@ -80,6 +81,7 @@ dependencies { testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.textcomposer.test) + testImplementation(projects.libraries.voicerecorder.test) testImplementation(libs.test.mockk) ksp(libs.showkase.processor) diff --git a/features/messages/impl/src/main/AndroidManifest.xml b/features/messages/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a00e8e1873 --- /dev/null +++ b/features/messages/impl/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + 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 6125e920b8..cf15db5bd9 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 @@ -63,6 +63,7 @@ import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -101,6 +102,7 @@ class MessagesPresenter @AssistedInject constructor( private val preferencesStore: PreferencesStore, private val featureFlagsService: FeatureFlagService, @Assisted private val navigator: MessagesNavigator, + private val buildMeta: BuildMeta, ) : Presenter { @AssistedFactory @@ -203,6 +205,7 @@ class MessagesPresenter @AssistedInject constructor( enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, enableInRoomCalls = enableInRoomCalls, + appName = buildMeta.applicationName, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 3a0585f390..81feec4b63 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -50,5 +50,6 @@ data class MessagesState( val enableTextFormatting: Boolean, val enableVoiceMessages: Boolean, val enableInRoomCalls: Boolean, + val appName: String, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 249c4b487e..ae279f6d49 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -86,5 +86,6 @@ fun aMessagesState() = MessagesState( enableTextFormatting = true, enableVoiceMessages = true, enableInRoomCalls = true, + appName = "Element", eventSink = {} ) 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 5a7168e7ce..6dd91aa01c 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 @@ -63,6 +63,8 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents +import io.element.android.features.messages.impl.voicemessages.VoiceMessagePermissionRationaleDialog import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule @@ -83,6 +85,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.LogCompositions +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.UserId @@ -107,6 +110,10 @@ fun MessagesView( ) { LogCompositions(tag = "MessagesScreen", msg = "Root") + OnLifecycleEvent { _, event -> + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event)) + } + AttachmentStateView( state = state.composerState.attachmentsState, onPreviewAttachments = onPreviewAttachments, @@ -306,6 +313,18 @@ private fun MessagesViewContent( enableTextFormatting = state.enableTextFormatting, ) + if (state.enableVoiceMessages && state.voiceMessageComposerState.showPermissionRationaleDialog) { + VoiceMessagePermissionRationaleDialog( + onContinue = { + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale) + }, + onDismiss = { + state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale) + }, + appName = state.appName + ) + } + ExpandableBottomSheetScaffold( sheetDragHandle = if (state.composerState.showTextFormatting) { @Composable { BottomSheetDragHandle() } 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 150ae23f9b..8f3899f139 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 @@ -71,10 +71,14 @@ internal fun MessageComposerView( } } - fun onVoiceRecordButtonEvent(press: PressEvent) { + val onVoiceRecordButtonEvent = { press: PressEvent -> voiceMessageState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(press)) } + fun onSendVoiceMessage() { + voiceMessageState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + } + TextComposer( modifier = modifier, state = state.richTextEditorState, @@ -89,7 +93,8 @@ internal fun MessageComposerView( onDismissTextFormatting = ::onDismissTextFormatting, enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, - onVoiceRecordButtonEvent = ::onVoiceRecordButtonEvent, + onVoiceRecordButtonEvent = onVoiceRecordButtonEvent, + onSendVoiceMessage = ::onSendVoiceMessage, onError = ::onError, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt index 7d6803fc41..42ba0d1d07 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerEvents.kt @@ -16,10 +16,15 @@ package io.element.android.features.messages.impl.voicemessages +import androidx.lifecycle.Lifecycle import io.element.android.libraries.textcomposer.model.PressEvent sealed interface VoiceMessageComposerEvents { data class RecordButtonEvent( val pressEvent: PressEvent ): VoiceMessageComposerEvents + data object SendVoiceMessage: VoiceMessageComposerEvents + data object AcceptPermissionRationale: VoiceMessageComposerEvents + data object DismissPermissionsRationale: VoiceMessageComposerEvents + data class LifecycleEvent(val event: Lifecycle.Event): VoiceMessageComposerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt index 106125934b..78e7d0ceb8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerPresenter.kt @@ -16,49 +16,171 @@ package io.element.android.features.messages.impl.voicemessages +import android.Manifest import androidx.compose.runtime.Composable +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.setValue +import androidx.core.net.toUri +import androidx.lifecycle.Lifecycle import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.mediaupload.api.MediaSender +import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.textcomposer.model.PressEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState +import io.element.android.libraries.voicerecorder.api.VoiceRecorder +import io.element.android.libraries.voicerecorder.api.VoiceRecorderState +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File import javax.inject.Inject @SingleIn(RoomScope::class) -class VoiceMessageComposerPresenter @Inject constructor() : Presenter { +class VoiceMessageComposerPresenter @Inject constructor( + private val appCoroutineScope: CoroutineScope, + private val voiceRecorder: VoiceRecorder, + private val analyticsService: AnalyticsService, + private val mediaSender: MediaSender, + permissionsPresenterFactory: PermissionsPresenter.Factory +) : Presenter { + private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO) + @Composable override fun present(): VoiceMessageComposerState { - var voiceMessageState by remember { mutableStateOf(VoiceMessageState.Idle) } + val localCoroutineScope = rememberCoroutineScope() + val recorderState by voiceRecorder.state.collectAsState(initial = VoiceRecorderState.Idle) - fun onRecordButtonPress(event: VoiceMessageComposerEvents.RecordButtonEvent) = when(event.pressEvent) { - PressEvent.PressStart -> { - // TODO start the recording - voiceMessageState = VoiceMessageState.Recording - } - PressEvent.LongPressEnd -> { - // TODO finish the recording - voiceMessageState = VoiceMessageState.Idle - } - PressEvent.Tapped -> { - // TODO discard the recording and show the 'hold to record' tooltip - voiceMessageState = VoiceMessageState.Idle + val permissionState = permissionsPresenter.present() + var isSending by remember { mutableStateOf(false) } + + val onLifecycleEvent = { event: Lifecycle.Event -> + when (event) { + Lifecycle.Event.ON_PAUSE -> { + appCoroutineScope.finishRecording() + } + Lifecycle.Event.ON_DESTROY -> { + appCoroutineScope.cancelRecording() + } + else -> {} } } + val onRecordButtonPress = { event: VoiceMessageComposerEvents.RecordButtonEvent -> + val permissionGranted = permissionState.permissionGranted + when (event.pressEvent) { + PressEvent.PressStart -> { + Timber.v("Voice message record button pressed") + when { + permissionGranted -> { + localCoroutineScope.startRecording() + } + else -> { + Timber.i("Voice message permission needed") + permissionState.eventSink(PermissionsEvents.RequestPermissions) + } + } + } + PressEvent.LongPressEnd -> { + Timber.v("Voice message record button released") + localCoroutineScope.finishRecording() + } + PressEvent.Tapped -> { + Timber.v("Voice message record button tapped") + localCoroutineScope.cancelRecording() + } + } + } - fun handleEvents(event: VoiceMessageComposerEvents) { + val onAcceptPermissionsRationale = { + permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog) + } + + val onDismissPermissionsRationale = { + permissionState.eventSink(PermissionsEvents.CloseDialog) + } + + val onSendButtonPress = lambda@{ + val finishedState = recorderState as? VoiceRecorderState.Finished + if (finishedState == null) { + val exception = VoiceMessageException.FileException("No file to send") + analyticsService.trackError(exception) + Timber.e(exception) + return@lambda + } + if (isSending) { + return@lambda + } + isSending = true + appCoroutineScope.sendMessage( + file = finishedState.file, + mimeType = finishedState.mimeType, + ).invokeOnCompletion { + isSending = false + } + } + + val handleEvents: (VoiceMessageComposerEvents) -> Unit = { event -> when (event) { is VoiceMessageComposerEvents.RecordButtonEvent -> onRecordButtonPress(event) + is VoiceMessageComposerEvents.SendVoiceMessage -> localCoroutineScope.launch { + onSendButtonPress() + } + VoiceMessageComposerEvents.DismissPermissionsRationale -> onDismissPermissionsRationale() + VoiceMessageComposerEvents.AcceptPermissionRationale -> onAcceptPermissionsRationale() + is VoiceMessageComposerEvents.LifecycleEvent -> onLifecycleEvent(event.event) } } return VoiceMessageComposerState( - voiceMessageState = voiceMessageState, - eventSink = { handleEvents(it) } + voiceMessageState = when (val state = recorderState) { + is VoiceRecorderState.Recording -> VoiceMessageState.Recording(level = state.level) + is VoiceRecorderState.Finished -> VoiceMessageState.Preview + else -> VoiceMessageState.Idle + }, + showPermissionRationaleDialog = permissionState.showDialog, + eventSink = handleEvents, ) } + + private fun CoroutineScope.startRecording() = launch { + try { + voiceRecorder.startRecord() + } catch (e: SecurityException) { + Timber.e(e, "Voice message error") + analyticsService.trackError(VoiceMessageException.PermissionMissing("Expected permission to record but none", e)) + } + } + + private fun CoroutineScope.finishRecording() = launch { + voiceRecorder.stopRecord() + } + + private fun CoroutineScope.cancelRecording() = launch { + voiceRecorder.stopRecord(cancelled = true) + } + + private fun CoroutineScope.sendMessage( + file: File, mimeType: String, + ) = launch { + val result = mediaSender.sendVoiceMessage( + uri = file.toUri(), + mimeType = mimeType, + waveForm = emptyList(), // TODO generate waveform + ) + + if (result.isFailure) { + Timber.e(result.exceptionOrNull(), "Voice message error") + return@launch + } + + voiceRecorder.deleteRecording() + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt index bacbe76324..8f0ab827b5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerState.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState @Stable data class VoiceMessageComposerState( val voiceMessageState: VoiceMessageState, + val showPermissionRationaleDialog: Boolean, val eventSink: (VoiceMessageComposerEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt index 63b59596c0..1a904beee3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageComposerStateProvider.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording), + aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(level = 0.5)), ) } @@ -30,5 +30,6 @@ internal fun aVoiceMessageComposerState( voiceMessageState: VoiceMessageState = VoiceMessageState.Idle, ) = VoiceMessageComposerState( voiceMessageState = voiceMessageState, + showPermissionRationaleDialog = false, eventSink = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt new file mode 100644 index 0000000000..2020b687ae --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt @@ -0,0 +1,26 @@ +/* + * 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.voicemessages + +internal sealed class VoiceMessageException : Exception() { + data class FileException( + override val message: String?, override val cause: Throwable? = null + ) : VoiceMessageException() + data class PermissionMissing( + override val message: String?, override val cause: Throwable? + ) : VoiceMessageException() +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessagePermissionRationaleDialog.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessagePermissionRationaleDialog.kt new file mode 100644 index 0000000000..19b7f7cb46 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessagePermissionRationaleDialog.kt @@ -0,0 +1,37 @@ +/* + * 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.voicemessages + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun VoiceMessagePermissionRationaleDialog( + onContinue: () -> Unit, + onDismiss: () -> Unit, + appName: String, +) { + ConfirmationDialog( + content = stringResource(CommonStrings.error_missing_microphone_voice_rationale_android, appName), + onSubmitClicked = onContinue, + onDismiss = onDismiss, + submitText = stringResource(CommonStrings.action_continue), + cancelText = stringResource(CommonStrings.action_cancel), + ) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index b02afd90fb..b0958a5d2d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -66,6 +66,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.mediapickers.test.FakePickerProvider @@ -75,6 +76,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate @@ -607,20 +609,28 @@ class MessagesPresenterTest { analyticsService: FakeAnalyticsService = FakeAnalyticsService(), permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), ): MessagesPresenter { + val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) + val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, room = matrixRoom, mediaPickerProvider = FakePickerProvider(), featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.NotificationSettings.key to true)), localMediaFactory = FakeLocalMediaFactory(mockMediaUrl), - mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom), + mediaSender = mediaSender, snackbarDispatcher = SnackbarDispatcher(), analyticsService = analyticsService, messageComposerContext = MessageComposerContextImpl(), richTextEditorStateFactory = TestRichTextEditorStateFactory(), - permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), + permissionsPresenterFactory = permissionsPresenterFactory, + ) + val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( + this, + FakeVoiceRecorder(), + analyticsService, + mediaSender, + permissionsPresenterFactory, ) - val voiceMessageComposerPresenter = VoiceMessageComposerPresenter() val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, @@ -649,6 +659,7 @@ class MessagesPresenterTest { clipboardHelper = clipboardHelper, preferencesStore = preferencesStore, featureFlagsService = FakeFeatureFlagService(), + buildMeta = aBuildMeta(), dispatchers = coroutineDispatchers, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt index 008226bf05..d1ee074e46 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt @@ -18,16 +18,31 @@ package io.element.android.features.messages.voicemessages +import android.Manifest +import androidx.lifecycle.Lifecycle import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow +import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerEvents import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerPresenter +import io.element.android.features.messages.impl.voicemessages.VoiceMessageComposerState +import io.element.android.features.messages.impl.voicemessages.VoiceMessageException +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.mediaupload.api.MediaSender +import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.aPermissionsState +import io.element.android.libraries.permissions.test.FakePermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.textcomposer.model.PressEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState +import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder +import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -37,53 +52,349 @@ class VoiceMessageComposerPresenterTest { @get:Rule val warmUpRule = WarmUpRule() + private val voiceRecorder = FakeVoiceRecorder() + private val analyticsService = FakeAnalyticsService() + private val matrixRoom = FakeMatrixRoom() + private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() } + private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom) + @Test fun `present - initial state`() = runTest { - val presenter = createPresenter() + val presenter = createVoiceMessageComposerPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + + testPauseAndDestroy(initialState) } } @Test fun `present - recording state`() = runTest { - val presenter = createPresenter() + val presenter = createVoiceMessageComposerPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Recording) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + + testPauseAndDestroy(finalState) } } @Test fun `present - abort recording`() = runTest { - val presenter = createPresenter() + val presenter = createVoiceMessageComposerPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.Tapped)) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + + testPauseAndDestroy(finalState) } } @Test fun `present - finish recording`() = runTest { - val presenter = createPresenter() + val presenter = createVoiceMessageComposerPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + + testPauseAndDestroy(finalState) } } - private fun createPresenter() = VoiceMessageComposerPresenter() + + @Test + fun `present - send recording`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(1) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - send recording before previous completed, waits`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().run { + eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + } + + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(1) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - send failures aren't tracked`() = runTest { + // Let sending fail due to media preprocessing error + mediaPreProcessor.givenResult(Result.failure(Exception())) + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + + val finalState = awaitItem().apply { + assertThat(voiceMessageState).isEqualTo(VoiceMessageState.Preview) + eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + } + assertThat(matrixRoom.sendMediaCount).isEqualTo(0) + assertThat(analyticsService.trackedErrors).hasSize(0) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - send failures can be retried`() = runTest { + // Let sending fail due to media preprocessing error + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + mediaPreProcessor.givenResult(Result.failure(Exception())) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + val previewState = awaitItem() + + previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + + ensureAllEventsConsumed() + assertThat(previewState.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + assertThat(matrixRoom.sendMediaCount).isEqualTo(0) + + mediaPreProcessor.givenAudioResult() + previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(1) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - send error - missing recording is tracked`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Send the message before recording anything + initialState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + + assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(0) + assertThat(analyticsService.trackedErrors).hasSize(1) + + testPauseAndDestroy(initialState) + } + } + + @Test + fun `present - record error - security exceptions are tracked`() = runTest { + val exception = SecurityException("") + voiceRecorder.givenThrowsSecurityException(exception) + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + + assertThat(matrixRoom.sendMediaCount).isEqualTo(0) + assertThat(analyticsService.trackedErrors).containsExactly( + VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception) + ) + + testPauseAndDestroy(initialState) + } + } + + @Test + fun `present - permission accepted first time`() = runTest { + val permissionsPresenter = createFakePermissionsPresenter( + recordPermissionGranted = false, + ) + val presenter = createVoiceMessageComposerPresenter( + permissionsPresenter = permissionsPresenter, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + + initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + permissionsPresenter.setPermissionGranted() + + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - permission denied previously`() = runTest { + val permissionsPresenter = createFakePermissionsPresenter( + recordPermissionGranted = false, + ) + val presenter = createVoiceMessageComposerPresenter( + permissionsPresenter = permissionsPresenter, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + + // See the dialog and accept it + awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(it.showPermissionRationaleDialog).isTrue() + it.eventSink(VoiceMessageComposerEvents.AcceptPermissionRationale) + } + + // Dialog is hidden, user accepts permissions + assertThat(awaitItem().showPermissionRationaleDialog).isFalse() + + permissionsPresenter.setPermissionGranted() + + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(0.2)) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - permission rationale dismissed`() = runTest { + val permissionsPresenter = createFakePermissionsPresenter( + recordPermissionGranted = false, + ) + val presenter = createVoiceMessageComposerPresenter( + permissionsPresenter = permissionsPresenter, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + + // See the dialog and accept it + awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(it.showPermissionRationaleDialog).isTrue() + it.eventSink(VoiceMessageComposerEvents.DismissPermissionsRationale) + } + + // Dialog is hidden, user tries to record again + awaitItem().also { + assertThat(it.showPermissionRationaleDialog).isFalse() + it.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + } + + // Dialog is shown once again + val finalState = awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(it.showPermissionRationaleDialog).isTrue() + } + + testPauseAndDestroy(finalState) + } + } + + private suspend fun TurbineTestContext.testPauseAndDestroy( + mostRecentState: VoiceMessageComposerState, + ) { + mostRecentState.eventSink( + VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE) + ) + + val onPauseState = when (mostRecentState.voiceMessageState) { + VoiceMessageState.Idle, + VoiceMessageState.Preview -> { + mostRecentState + } + is VoiceMessageState.Recording -> { + awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + } + } + } + + onPauseState.eventSink( + VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY) + ) + + when (onPauseState.voiceMessageState) { + VoiceMessageState.Idle -> + ensureAllEventsConsumed() + is VoiceMessageState.Recording, + VoiceMessageState.Preview -> + assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + } + } + + private fun TestScope.createVoiceMessageComposerPresenter( + permissionsPresenter: PermissionsPresenter = createFakePermissionsPresenter(), + ): VoiceMessageComposerPresenter { + return VoiceMessageComposerPresenter( + this, + voiceRecorder, + analyticsService, + mediaSender, + FakePermissionsPresenterFactory(permissionsPresenter), + ) + } + + private fun createFakePermissionsPresenter( + recordPermissionGranted: Boolean = true, + recordPermissionShowDialog: Boolean = false, + ): FakePermissionsPresenter { + val initialPermissionState = aPermissionsState( + showDialog = recordPermissionShowDialog, + permission = Manifest.permission.RECORD_AUDIO, + permissionGranted = recordPermissionGranted, + ) + return FakePermissionsPresenter( + initialState = initialPermissionState + ) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c86738fa0..7c3afb48dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -75,6 +75,7 @@ google_firebase_bom = "com.google.firebase:firebase-bom:32.4.0" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" } +androidx_annotationjvm = { module = "androidx.annotation:annotation-jvm", version = "1.7.0" } androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx_exifinterface = "androidx.exifinterface:exifinterface:1.3.6" @@ -164,6 +165,7 @@ statemachine = "com.freeletics.flowredux:compose:1.2.0" maplibre = "org.maplibre.gl:android-sdk:10.2.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.1" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" +opusencoder = "io.element.android:opusencoder:1.1.0" # Analytics posthog = "com.posthog.android:posthog:2.0.3" diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/hash/Hash.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/hash/Hash.kt new file mode 100644 index 0000000000..760431a7be --- /dev/null +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/hash/Hash.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.core.hash + +import java.security.MessageDigest +import java.util.Locale + +/** + * Compute a Hash of a String, using md5 algorithm. + */ +fun String.md5() = try { + val digest = MessageDigest.getInstance("md5") + val locale = Locale.ROOT + digest.update(toByteArray()) + digest.digest() + .joinToString("") { String.format(locale, "%02X", it) } + .lowercase(locale) +} catch (exc: Exception) { + // Should not happen, but just in case + hashCode().toString() +} diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 899e92efc5..dde62e7513 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -50,16 +50,43 @@ class MediaSender @Inject constructor( .flatMapCatching { info -> room.sendMedia(info, progressCallback) } - .onFailure { error -> - val job = ongoingUploadJobs.remove(Job) - if (error !is CancellationException) { - job?.cancel() - } - } - .onSuccess { - ongoingUploadJobs.remove(Job) - } + .handleSendResult() } + suspend fun sendVoiceMessage( + uri: Uri, + mimeType: String, + waveForm: List, + progressCallback: ProgressCallback? = null + ): Result { + return preProcessor + .process( + uri = uri, + mimeType = mimeType, + deleteOriginal = true, + compressIfPossible = false + ) + .flatMapCatching { info -> + val audioInfo = (info as MediaUploadInfo.Audio).audioInfo + val newInfo = MediaUploadInfo.VoiceMessage( + file = info.file, + audioInfo = audioInfo, + waveform = waveForm, + ) + room.sendMedia(newInfo, progressCallback) + } + .handleSendResult() + } + + private fun Result.handleSendResult() = this + .onFailure { error -> + val job = ongoingUploadJobs.remove(Job) + if (error !is CancellationException) { + job?.cancel() + } + } + .onSuccess { + ongoingUploadJobs.remove(Job) + } private suspend fun MatrixRoom.sendMedia( uploadInfo: MediaUploadInfo, @@ -90,7 +117,14 @@ class MediaSender @Inject constructor( progressCallback = progressCallback ) } - + is MediaUploadInfo.VoiceMessage -> { + sendVoiceMessage( + file = uploadInfo.file, + audioInfo = uploadInfo.audioInfo, + waveform = uploadInfo.waveform, + progressCallback = progressCallback + ) + } is MediaUploadInfo.AnyFile -> { sendFile( file = uploadInfo.file, diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt index 51f6372b23..e1debf6bda 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt @@ -29,5 +29,6 @@ sealed interface MediaUploadInfo { data class Image(override val file: File, val imageInfo: ImageInfo, val thumbnailFile: File) : MediaUploadInfo data class Video(override val file: File, val videoInfo: VideoInfo, val thumbnailFile: File) : MediaUploadInfo data class Audio(override val file: File, val audioInfo: AudioInfo) : MediaUploadInfo + data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List) : MediaUploadInfo data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index cd968530f8..205be3b241 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -118,6 +118,7 @@ class AndroidMediaPreProcessor @Inject constructor( is MediaUploadInfo.Audio -> copy(file = renamedFile) is MediaUploadInfo.Image -> copy(file = renamedFile) is MediaUploadInfo.Video -> copy(file = renamedFile) + is MediaUploadInfo.VoiceMessage -> copy(file = renamedFile) } } diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt index d94414d2d7..8e7e71e8fb 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt @@ -17,11 +17,14 @@ package io.element.android.libraries.mediaupload.test import android.net.Uri +import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.tests.testutils.simulateLongTask import java.io.File +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration class FakeMediaPreProcessor : MediaPreProcessor { @@ -53,4 +56,19 @@ class FakeMediaPreProcessor : MediaPreProcessor { fun givenResult(value: Result) { this.result = value } + + fun givenAudioResult() { + givenResult( + Result.success( + MediaUploadInfo.Audio( + file = File("audio.ogg"), + audioInfo = AudioInfo( + duration = 1000.seconds.toJavaDuration(), + size = 1000, + mimetype = "audio/ogg", + ), + ) + ) + ) + } } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt index cc59d96b44..19797b9075 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt @@ -31,10 +31,11 @@ open class PermissionsStateProvider : PreviewParameterProvider fun aPermissionsState( showDialog: Boolean, - permission: String = Manifest.permission.POST_NOTIFICATIONS + permission: String = Manifest.permission.POST_NOTIFICATIONS, + permissionGranted: Boolean = false, ) = PermissionsState( permission = permission, - permissionGranted = false, + permissionGranted = permissionGranted, shouldShowRationale = false, showDialog = showDialog, permissionAlreadyAsked = false, 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 911cebb142..7924077394 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 @@ -64,7 +64,8 @@ import io.element.android.libraries.testtags.testTag import io.element.android.libraries.textcomposer.components.ComposerOptionsButton import io.element.android.libraries.textcomposer.components.DismissTextFormattingButton import io.element.android.libraries.textcomposer.components.RecordButton -import io.element.android.libraries.textcomposer.components.RecordingProgress +import io.element.android.libraries.textcomposer.components.VoiceMessagePreview +import io.element.android.libraries.textcomposer.components.VoiceMessageRecording import io.element.android.libraries.textcomposer.components.SendButton import io.element.android.libraries.textcomposer.components.TextFormatting import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape @@ -95,6 +96,7 @@ fun TextComposer( onAddAttachment: () -> Unit = {}, onDismissTextFormatting: () -> Unit = {}, onVoiceRecordButtonEvent: (PressEvent) -> Unit = {}, + onSendVoiceMessage: () -> Unit = {}, onError: (Throwable) -> Unit = {}, ) { val onSendClicked = { @@ -137,24 +139,39 @@ fun TextComposer( composerMode = composerMode, ) } - val recordButton = @Composable { + val recordVoiceButton = @Composable { RecordButton( onPressStart = { onVoiceRecordButtonEvent(PressEvent.PressStart) }, onLongPressEnd = { onVoiceRecordButtonEvent(PressEvent.LongPressEnd) }, onTap = { onVoiceRecordButtonEvent(PressEvent.Tapped) }, ) } + val sendVoiceButton = @Composable { + SendButton( + canSendMessage = voiceMessageState is VoiceMessageState.Preview, + onClick = { onSendVoiceMessage() }, + composerMode = composerMode, + ) + } val textFormattingOptions = @Composable { TextFormatting(state = state) } - val sendOrRecordButton = if (canSendMessage || !enableVoiceMessages) { - sendButton - } else { - recordButton + val sendOrRecordButton = when { + enableVoiceMessages && !canSendMessage -> + when (voiceMessageState) { + is VoiceMessageState.Preview -> sendVoiceButton + else -> recordVoiceButton + } + else -> + sendButton } - val recordingProgress = @Composable { - RecordingProgress() + val voiceRecording = @Composable { + if (voiceMessageState is VoiceMessageState.Recording) { + VoiceMessageRecording(voiceMessageState.level) + } else if (voiceMessageState is VoiceMessageState.Preview) { + VoiceMessagePreview() + } } if (showTextFormatting) { @@ -170,11 +187,12 @@ fun TextComposer( } else { StandardLayout( voiceMessageState = voiceMessageState, + enableVoiceMessages = enableVoiceMessages, modifier = layoutModifier, composerOptionsButton = composerOptionsButton, textInput = textInput, endButton = sendOrRecordButton, - recordingProgress = recordingProgress, + voiceRecording = voiceRecording, ) } @@ -190,9 +208,10 @@ fun TextComposer( @Composable private fun StandardLayout( voiceMessageState: VoiceMessageState, + enableVoiceMessages: Boolean, textInput: @Composable () -> Unit, composerOptionsButton: @Composable () -> Unit, - recordingProgress: @Composable () -> Unit, + voiceRecording: @Composable () -> Unit, endButton: @Composable () -> Unit, modifier: Modifier = Modifier, ) { @@ -200,13 +219,13 @@ private fun StandardLayout( modifier = modifier, verticalAlignment = Alignment.Bottom, ) { - if (voiceMessageState is VoiceMessageState.Recording) { + if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) { Box( modifier = Modifier .padding(start = 16.dp, bottom = 8.dp, top = 8.dp) .weight(1f) ) { - recordingProgress() + voiceRecording() } } else { Box( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt similarity index 75% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 2fc0420e05..351293a329 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -17,14 +17,10 @@ package io.element.android.libraries.textcomposer.components import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -36,7 +32,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @Composable -internal fun RecordingProgress( +internal fun VoiceMessagePreview( modifier: Modifier = Modifier, ) { Row( @@ -50,16 +46,9 @@ internal fun RecordingProgress( .heightIn(26.dp), verticalAlignment = Alignment.CenterVertically, ) { - Box( - modifier = Modifier - .size(8.dp) - .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) - ) - Spacer(Modifier.size(8.dp)) - - // TODO Replace with timer UI + // TODO Replace with recording preview UI Text( - text = "Recording...", // Not localized because it is a placeholder + text = "Finished recording", // Not localized because it is a placeholder color = ElementTheme.colors.textSecondary, style = ElementTheme.typography.fontBodySmMedium ) @@ -68,6 +57,6 @@ internal fun RecordingProgress( @PreviewsDayNight @Composable -internal fun RecordingProgressPreview() = ElementPreview { - RecordingProgress() +internal fun VoiceMessagePreviewPreview() = ElementPreview { + VoiceMessagePreview() } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt new file mode 100644 index 0000000000..24703a579c --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -0,0 +1,102 @@ +/* + * 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.textcomposer.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +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 +import io.element.android.libraries.theme.ElementTheme + +@Composable +internal fun VoiceMessageRecording( + level: Double, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = ElementTheme.colors.bgSubtleSecondary, + shape = MaterialTheme.shapes.medium, + ) + .padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) + .heightIn(26.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(8.dp) + .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) + ) + Spacer(Modifier.size(8.dp)) + + // TODO Replace with timer UI + Text( + text = "Recording...", // Not localized because it is a placeholder + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodySmMedium + ) + + Spacer(Modifier.size(20.dp)) + + // TODO Replace with waveform UI + DebugAudioLevel( + modifier = Modifier.weight(1f), level = level + ) + } +} + +@Composable +private fun DebugAudioLevel( + level: Double, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .height(26.dp) + ) { + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .fillMaxWidth(level.toFloat()) + .background(ElementTheme.colors.iconQuaternary, shape = MaterialTheme.shapes.small) + .fillMaxHeight() + ) + } +} + +@PreviewsDayNight +@Composable +internal fun VoiceMessageRecordingPreview() = ElementPreview { + VoiceMessageRecording(0.5) +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index d376c4ee70..835000478a 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -18,5 +18,9 @@ package io.element.android.libraries.textcomposer.model sealed class VoiceMessageState { data object Idle: VoiceMessageState() - data object Recording: VoiceMessageState() + + data object Preview: VoiceMessageState() + data class Recording( + val level: Double, + ): VoiceMessageState() } diff --git a/libraries/voicerecorder/api/build.gradle.kts b/libraries/voicerecorder/api/build.gradle.kts new file mode 100644 index 0000000000..bed69b7d28 --- /dev/null +++ b/libraries/voicerecorder/api/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * 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") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.voicerecorder.api" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.androidx.annotationjvm) + implementation(libs.coroutines.core) +} diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorder.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorder.kt new file mode 100644 index 0000000000..77465ddeea --- /dev/null +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorder.kt @@ -0,0 +1,55 @@ +/* + * 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.voicerecorder.api + +import android.Manifest +import androidx.annotation.RequiresPermission +import kotlinx.coroutines.flow.StateFlow + +/** + * Audio recorder which records audio to opus/ogg files. + */ +interface VoiceRecorder { + /** + * Start a recording. + * + * Call [stopRecord] to stop the recording and release resources. + */ + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + suspend fun startRecord() + + /** + * Stop the current recording. + * + * Call [deleteRecording] to delete any recorded audio. + * + * @param cancelled If true, the recording is deleted. + */ + suspend fun stopRecord( + cancelled: Boolean = false + ) + + /** + * Stop the current recording and delete the output file. + */ + suspend fun deleteRecording() + + /** + * The current state of the recorder. + */ + val state: StateFlow +} diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt new file mode 100644 index 0000000000..8d531c3565 --- /dev/null +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt @@ -0,0 +1,44 @@ +/* + * 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.voicerecorder.api + +import java.io.File + +sealed class VoiceRecorderState { + /** + * The recorder is idle and not recording. + */ + data object Idle : VoiceRecorderState() + + /** + * The recorder is currently recording. + * + * @property level The current audio level of the recording as a fraction of 1. + */ + data class Recording(val level: Double) : VoiceRecorderState() + + /** + * The recorder has finished recording. + * + * @property file The recorded file. + * @property mimeType The mime type of the file. + */ + data class Finished( + val file: File, + val mimeType: String, + ) : VoiceRecorderState() +} diff --git a/libraries/voicerecorder/impl/build.gradle.kts b/libraries/voicerecorder/impl/build.gradle.kts new file mode 100644 index 0000000000..6ebfb28997 --- /dev/null +++ b/libraries/voicerecorder/impl/build.gradle.kts @@ -0,0 +1,48 @@ +/* + * 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") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.voicerecorder.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + api(projects.libraries.voicerecorder.api) + api(libs.opusencoder) + + implementation(libs.dagger) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + + implementation(libs.androidx.annotationjvm) + implementation(libs.coroutines.core) + + testImplementation(projects.tests.testutils) + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.mockk) + testImplementation(libs.test.turbine) + testImplementation(libs.coroutines.core) + testImplementation(libs.coroutines.test) +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt new file mode 100644 index 0000000000..ef91118371 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -0,0 +1,132 @@ +/* + * 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.voicerecorder.impl + +import android.Manifest +import androidx.annotation.RequiresPermission +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.voicerecorder.api.VoiceRecorder +import io.element.android.libraries.voicerecorder.api.VoiceRecorderState +import io.element.android.libraries.voicerecorder.impl.audio.Audio +import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig +import io.element.android.libraries.voicerecorder.impl.audio.AudioLevelCalculator +import io.element.android.libraries.voicerecorder.impl.audio.AudioReader +import io.element.android.libraries.voicerecorder.impl.audio.Encoder +import io.element.android.libraries.voicerecorder.impl.file.VoiceFileConfig +import io.element.android.libraries.voicerecorder.impl.file.VoiceFileManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File +import java.util.UUID +import javax.inject.Inject + +@SingleIn(RoomScope::class) +@ContributesBinding(RoomScope::class) +class VoiceRecorderImpl @Inject constructor( + private val dispatchers: CoroutineDispatchers, + private val audioReaderFactory: AudioReader.Factory, + private val encoder: Encoder, + private val fileManager: VoiceFileManager, + private val config: AudioConfig, + private val fileConfig: VoiceFileConfig, + private val audioLevelCalculator: AudioLevelCalculator, + appCoroutineScope: CoroutineScope, +) : VoiceRecorder { + private val voiceCoroutineScope by lazy { + appCoroutineScope.childScope(dispatchers.io, "VoiceRecorder-${UUID.randomUUID()}") + } + + private var outputFile: File? = null + private var audioReader: AudioReader? = null + private var recordingJob: Job? = null + + private val _state = MutableStateFlow(VoiceRecorderState.Idle) + override val state: StateFlow = _state + + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + override suspend fun startRecord() { + Timber.i("Voice recorder started recording") + outputFile = fileManager.createFile() + .also(encoder::init) + + val audioRecorder = audioReaderFactory.create(config, dispatchers).also { audioReader = it } + + recordingJob = voiceCoroutineScope.launch { + audioRecorder.record { audio -> + when (audio) { + is Audio.Data -> { + val audioLevel = audioLevelCalculator.calculateAudioLevel(audio.buffer) + _state.emit(VoiceRecorderState.Recording(audioLevel)) + encoder.encode(audio.buffer, audio.readSize) + } + is Audio.Error -> { + Timber.e("Voice message error: code=${audio.audioRecordErrorCode}") + _state.emit(VoiceRecorderState.Recording(0.0)) + } + } + } + } + } + + /** + * Stop the current recording. + * + * Call [deleteRecording] to delete any recorded audio. + */ + override suspend fun stopRecord( + cancelled: Boolean + ) { + recordingJob?.cancel()?.also { + Timber.i("Voice recorder stopped recording") + } + recordingJob = null + + audioReader?.stop() + audioReader = null + encoder.release() + + if (cancelled) { + deleteRecording() + } + + _state.emit( + when (val file = outputFile) { + null -> VoiceRecorderState.Idle + else -> VoiceRecorderState.Finished(file, fileConfig.mimeType) + } + ) + } + + /** + * Stop the current recording and delete the output file. + */ + override suspend fun deleteRecording() { + outputFile?.let(fileManager::deleteFile)?.also { + Timber.i("Voice recorder deleted recording") + } + outputFile = null + _state.emit(VoiceRecorderState.Idle) + } +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt new file mode 100644 index 0000000000..a2342f3c2f --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AndroidAudioReader.kt @@ -0,0 +1,139 @@ +/* + * 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.voicerecorder.impl.audio + +import android.Manifest +import android.media.AudioRecord +import android.media.audiofx.AutomaticGainControl +import android.media.audiofx.NoiseSuppressor +import androidx.annotation.RequiresPermission +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.di.RoomScope +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext + +class AndroidAudioReader +@RequiresPermission(Manifest.permission.RECORD_AUDIO) private constructor( + private val config: AudioConfig, + private val dispatchers: CoroutineDispatchers, +) : AudioReader { + private val audioRecord: AudioRecord + private var noiseSuppressor: NoiseSuppressor? = null + private var automaticGainControl: AutomaticGainControl? = null + private val outputBuffer: ShortArray + + init { + outputBuffer = createOutputBuffer(config.sampleRate) + audioRecord = AudioRecord.Builder().setAudioSource(config.source).setAudioFormat(config.format).setBufferSizeInBytes(outputBuffer.sizeInBytes()).build() + noiseSuppressor = requestNoiseSuppressor(audioRecord) + automaticGainControl = requestAutomaticGainControl(audioRecord) + } + + /** + * Record audio data continuously. + * + * @param onAudio callback when audio is read. + */ + override suspend fun record( + onAudio: suspend (Audio) -> Unit, + ) { + audioRecord.startRecording() + withContext(dispatchers.io) { + while (isActive) { + if (audioRecord.recordingState != AudioRecord.RECORDSTATE_RECORDING) { + break + } + onAudio(read()) + } + } + } + + private fun read(): Audio { + val result = audioRecord.read(outputBuffer, 0, outputBuffer.size) + + if (isAudioRecordErrorResult(result)) { + return Audio.Error(result) + } + + return Audio.Data( + result, + outputBuffer, + ) + } + + override fun stop() { + if (audioRecord.state == AudioRecord.STATE_INITIALIZED) { + audioRecord.stop() + } + audioRecord.release() + + noiseSuppressor?.release() + noiseSuppressor = null + + automaticGainControl?.release() + automaticGainControl = null + } + + private fun createOutputBuffer(sampleRate: SampleRate): ShortArray { + val bufferSizeInShorts = AudioRecord.getMinBufferSize( + sampleRate.hz, + config.format.channelMask, + config.format.encoding + ) + return ShortArray(bufferSizeInShorts) + } + + private fun requestNoiseSuppressor(audioRecord: AudioRecord): NoiseSuppressor? { + if (!NoiseSuppressor.isAvailable()) { + return null + } + + return tryOrNull { + NoiseSuppressor.create(audioRecord.audioSessionId).apply { + enabled = true + } + } + } + + private fun requestAutomaticGainControl(audioRecord: AudioRecord): AutomaticGainControl? { + if (!AutomaticGainControl.isAvailable()) { + return null + } + + return tryOrNull { + AutomaticGainControl.create(audioRecord.audioSessionId).apply { + enabled = true + } + } + } + + @ContributesBinding(RoomScope::class) + companion object Factory : AudioReader.Factory { + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + override fun create(config: AudioConfig, dispatchers: CoroutineDispatchers): AndroidAudioReader { + return AndroidAudioReader(config, dispatchers) + } + } +} + +private fun isAudioRecordErrorResult(result: Int): Boolean { + return result < 0 +} + +private fun ShortArray.sizeInBytes(): Int = size * Short.SIZE_BYTES diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Audio.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Audio.kt new file mode 100644 index 0000000000..3e51d615f4 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Audio.kt @@ -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. + */ + +package io.element.android.libraries.voicerecorder.impl.audio + +sealed class Audio { + class Data( + val readSize: Int, + val buffer: ShortArray, + ) : Audio() + + data class Error( + val audioRecordErrorCode: Int + ) : Audio() +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioConfig.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioConfig.kt new file mode 100644 index 0000000000..6ff912c2ae --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioConfig.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.voicerecorder.impl.audio + +import android.media.AudioFormat +import android.media.MediaRecorder.AudioSource + +/** + * Audio configuration for voice recording. + * + * @property source the audio source to use, see constants in [AudioSource] + * @property format the audio format to use, see [AudioFormat] + * @property sampleRate the sample rate to use. Ensure this matches the value set in [format]. + * @property bitRate the bitrate in bps + */ +data class AudioConfig( + val source: Int, + val format: AudioFormat, + val sampleRate: SampleRate, + val bitRate: Int, +) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt new file mode 100644 index 0000000000..554b6ba4b1 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt @@ -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. + */ + +package io.element.android.libraries.voicerecorder.impl.audio + +interface AudioLevelCalculator { + /** + * Calculate the audio level of the audio buffer. + * + * @param buffer The audio buffer containing raw audio data. + * + * @return A value between 0 and 1. + */ + fun calculateAudioLevel(buffer: ShortArray): Double +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioReader.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioReader.kt new file mode 100644 index 0000000000..230c9533fd --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioReader.kt @@ -0,0 +1,37 @@ +/* + * 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.voicerecorder.impl.audio + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers + +interface AudioReader { + /** + * Record audio data continuously. + * + * @param onAudio callback when audio is read. + */ + suspend fun record( + onAudio: suspend (Audio) -> Unit, + ) + + fun stop() + + interface Factory { + fun create(config: AudioConfig, dispatchers: CoroutineDispatchers): AudioReader + } + +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt new file mode 100644 index 0000000000..8a16acf83b --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt @@ -0,0 +1,49 @@ +/* + * 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.voicerecorder.impl.audio + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import javax.inject.Inject +import kotlin.math.log10 +import kotlin.math.min +import kotlin.math.sqrt + +@ContributesBinding(RoomScope::class) +class DecibelAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { + companion object { + private const val REFERENCE_DB = 50.0 // Reference dB for normal conversation + } + + override fun calculateAudioLevel(buffer: ShortArray): Double { + val rms = buffer.rootMeanSquare() + + // Convert to decibels and clip + val db = 20 * log10(rms / REFERENCE_DB) + val clipped = min(db, REFERENCE_DB) + + // Scale to the range [0.0, 1.0] + return clipped / REFERENCE_DB + } + + private fun ShortArray.rootMeanSquare(): Double { + // Use Double to avoid overflow + val sumOfSquares: Double = sumOf { it.toDouble() * it.toDouble() } + val avgSquare = sumOfSquares / size.toDouble() + return sqrt(avgSquare) + } +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt new file mode 100644 index 0000000000..a888824fe5 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt @@ -0,0 +1,63 @@ +/* + * 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.voicerecorder.impl.audio + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import io.element.android.opusencoder.OggOpusEncoder +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import javax.inject.Provider + +/** + * Safe wrapper for OggOpusEncoder. + */ +@ContributesBinding(RoomScope::class) +class DefaultEncoder @Inject constructor( + private val encoderProvider: Provider, + config: AudioConfig, +) : Encoder { + private val bitRate = config.bitRate +private val sampleRate = config.sampleRate.asEncoderModel() + + private var encoder: OggOpusEncoder? = null + override fun init( + file: File, + ) { + encoder?.release() + encoder = encoderProvider.get().apply { + init(file.absolutePath, sampleRate) + setBitrate(bitRate) + // TODO check encoder application: 2048 (voice, default is typically 2049 as audio) + } + } + + override fun encode( + buffer: ShortArray, + readSize: Int, + ) { + encoder?.encode(buffer, readSize) + ?: Timber.w("Can't encode when encoder not initialized") + } + + override fun release() { + encoder?.release() + ?: Timber.w("Can't release encoder that is not initialized") + encoder = null + } +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Encoder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Encoder.kt new file mode 100644 index 0000000000..67685635aa --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Encoder.kt @@ -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. + */ + +package io.element.android.libraries.voicerecorder.impl.audio + +import java.io.File + +interface Encoder { + + fun init(file: File) + + fun encode(buffer: ShortArray, readSize: Int) + + fun release() +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/SampleRate.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/SampleRate.kt new file mode 100644 index 0000000000..b392b6e19f --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/SampleRate.kt @@ -0,0 +1,24 @@ +/* + * 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.voicerecorder.impl.audio + +import io.element.android.opusencoder.configuration.SampleRate as LibOpusOggSampleRate + +data object SampleRate { + const val hz = 48_000 + fun asEncoderModel() = LibOpusOggSampleRate.Rate48kHz +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt new file mode 100644 index 0000000000..b21ab48ac3 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt @@ -0,0 +1,58 @@ +/* + * 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.voicerecorder.impl.di + +import android.media.AudioFormat +import android.media.MediaRecorder +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig +import io.element.android.libraries.voicerecorder.impl.audio.SampleRate +import io.element.android.libraries.voicerecorder.impl.file.VoiceFileConfig +import io.element.android.opusencoder.OggOpusEncoder + +@Module +@ContributesTo(RoomScope::class) +object VoiceRecorderModule { + @Provides + fun provideAudioConfig(): AudioConfig { + val sampleRate = SampleRate + return AudioConfig( + format = AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(sampleRate.hz) + .setChannelMask(AudioFormat.CHANNEL_IN_MONO) + .build(), + bitRate = 24_000, // 24 kbps + sampleRate = sampleRate, + source = MediaRecorder.AudioSource.MIC, + ) + } + + @Provides + fun provideVoiceFileConfig(): VoiceFileConfig = + VoiceFileConfig( + cacheSubdir = "voice_recordings", + fileExt = "ogg", + mimeType = "audio/ogg", + ) + + @Provides + fun provideOggOpusEncoder(): OggOpusEncoder = OggOpusEncoder.create() +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt new file mode 100644 index 0000000000..07ef54991f --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/DefaultVoiceFileManager.kt @@ -0,0 +1,49 @@ +/* + * 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.voicerecorder.impl.file + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.hash.md5 +import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import java.io.File +import java.util.UUID +import javax.inject.Inject + +@ContributesBinding(RoomScope::class) +class DefaultVoiceFileManager @Inject constructor( + @CacheDirectory private val cacheDir: File, + private val config: VoiceFileConfig, + room: MatrixRoom, +) : VoiceFileManager { + + private val roomId: RoomId = room.roomId + + override fun createFile(): File { + val fileName = "${UUID.randomUUID()}.${config.fileExt}" + val outputDirectory = File(cacheDir, config.cacheSubdir) + val roomDir = File(outputDirectory, roomId.value.md5()) + .apply(File::mkdirs) + return File(roomDir, fileName) + } + + override fun deleteFile(file: File) { + file.delete() + } +} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileConfig.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileConfig.kt new file mode 100644 index 0000000000..a7b1f4607d --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileConfig.kt @@ -0,0 +1,30 @@ +/* + * 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.voicerecorder.impl.file + +/** + * File configuration for voice recording. + * + * @property cacheSubdir the subdirectory in the cache dir to use. + * @property fileExt the file extension for audio files. + * @property mimeType the mime type of audio files. + */ +data class VoiceFileConfig( + val cacheSubdir: String, + val fileExt: String, + val mimeType: String, +) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileManager.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileManager.kt new file mode 100644 index 0000000000..77e85b910e --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/file/VoiceFileManager.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.voicerecorder.impl.file + +import java.io.File + +interface VoiceFileManager { + fun createFile(): File + + fun deleteFile(file: File) +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt new file mode 100644 index 0000000000..847e1c514f --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt @@ -0,0 +1,134 @@ +/* + * 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.voicerecorder.impl + +import android.media.AudioFormat +import android.media.MediaRecorder +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.voicerecorder.api.VoiceRecorderState +import io.element.android.libraries.voicerecorder.impl.audio.Audio +import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig +import io.element.android.libraries.voicerecorder.impl.audio.SampleRate +import io.element.android.libraries.voicerecorder.impl.di.VoiceRecorderModule +import io.element.android.libraries.voicerecorder.test.FakeAudioLevelCalculator +import io.element.android.libraries.voicerecorder.test.FakeAudioRecorderFactory +import io.element.android.libraries.voicerecorder.test.FakeEncoder +import io.element.android.libraries.voicerecorder.test.FakeFileSystem +import io.element.android.libraries.voicerecorder.test.FakeVoiceFileManager +import io.element.android.tests.testutils.testCoroutineDispatchers +import io.mockk.mockk +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.BeforeClass +import org.junit.Test +import java.io.File + +class VoiceRecorderImplTest { + private val fakeFileSystem = FakeFileSystem() + + @Test + fun `it emits the initial state`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + } + } + + @Test + fun `when recording, it emits the recording state`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + + voiceRecorder.startRecord() + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.0)) + } + } + + @Test + fun `when stopped, it provides a file`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + + voiceRecorder.startRecord() + skipItems(3) + voiceRecorder.stopRecord() + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Finished(File(FILE_PATH), "audio/ogg")) + assertThat(fakeFileSystem.files[File(FILE_PATH)]).isEqualTo(ENCODED_DATA) + } + } + + @Test + fun `when cancelled, it deletes the file`() = runTest { + val voiceRecorder = createVoiceRecorder() + voiceRecorder.state.test { + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + + voiceRecorder.startRecord() + skipItems(3) + voiceRecorder.stopRecord(cancelled = true) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) + assertThat(fakeFileSystem.files[File(FILE_PATH)]).isNull() + } + } + + private fun TestScope.createVoiceRecorder(): VoiceRecorderImpl { + val fileConfig = VoiceRecorderModule.provideVoiceFileConfig() + return VoiceRecorderImpl( + dispatchers = testCoroutineDispatchers(), + audioReaderFactory = FakeAudioRecorderFactory( + audio = AUDIO, + ), + encoder = FakeEncoder(fakeFileSystem), + config = AudioConfig( + format = AUDIO_FORMAT, + bitRate = 24_000, // 24 kbps + sampleRate = SampleRate, + source = MediaRecorder.AudioSource.MIC, + ), + fileConfig = fileConfig, + fileManager = FakeVoiceFileManager(fakeFileSystem, fileConfig, FILE_ID), + audioLevelCalculator = FakeAudioLevelCalculator(), + appCoroutineScope = backgroundScope, + ) + } + + companion object { + const val FILE_ID: String = "recording" + const val FILE_PATH = "voice_recordings/${FILE_ID}.ogg" + private lateinit var AUDIO_FORMAT: AudioFormat + + // FakeEncoder doesn't actually encode, it just writes the data to the file + private const val ENCODED_DATA = "[32767, 32767, 32767][32767, 32767, 32767]" + private const val MAX_AMP = Short.MAX_VALUE + private val AUDIO = listOf( + Audio.Data(3, shortArrayOf(MAX_AMP, MAX_AMP, MAX_AMP)), + Audio.Error(-1), + Audio.Data(3, shortArrayOf(MAX_AMP, MAX_AMP, MAX_AMP)), + ) + + @BeforeClass + @JvmStatic + fun initAudioFormat() { + AUDIO_FORMAT = mockk() + } + } +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt new file mode 100644 index 0000000000..8ffbf1ef8e --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt @@ -0,0 +1,46 @@ +/* + * 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.voicerecorder.impl.audio + +import org.junit.Test + +class DecibelAudioLevelCalculatorTest { + + @Test + fun `given max values, it returns values within range`() { + val calculator = DecibelAudioLevelCalculator() + val buffer = ShortArray(100) { Short.MAX_VALUE } + val level = calculator.calculateAudioLevel(buffer) + assert(level in 0.0..1.0) + } + + @Test + fun `given mixed values, it returns values within range`() { + val calculator = DecibelAudioLevelCalculator() + val buffer = shortArrayOf(Short.MAX_VALUE, Short.MIN_VALUE, -1, 1) + val level = calculator.calculateAudioLevel(buffer) + assert(level in 0.0..1.0) + } + + @Test + fun `given min values, it returns values within range`() { + val calculator = DecibelAudioLevelCalculator() + val buffer = ShortArray(100) { Short.MIN_VALUE } + val level = calculator.calculateAudioLevel(buffer) + assert(level in 0.0..1.0) + } +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt new file mode 100644 index 0000000000..1615067f6c --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt @@ -0,0 +1,26 @@ +/* + * 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.voicerecorder.test + +import io.element.android.libraries.voicerecorder.impl.audio.AudioLevelCalculator +import kotlin.math.abs + +class FakeAudioLevelCalculator: AudioLevelCalculator { + override fun calculateAudioLevel(buffer: ShortArray): Double { + return buffer.map { abs(it.toDouble()) }.average() / Short.MAX_VALUE + } +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt new file mode 100644 index 0000000000..71fd2df041 --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioReader.kt @@ -0,0 +1,49 @@ +/* + * 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.voicerecorder.test + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.voicerecorder.impl.audio.Audio +import io.element.android.libraries.voicerecorder.impl.audio.AudioReader +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield + +class FakeAudioReader( + private val dispatchers: CoroutineDispatchers, + private val audio: List diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index c695309194..9296381c87 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -1,12 +1,18 @@ - "Please wait for this to complete before signing out." - "Your keys are still being backed up" "Are you sure you want to sign out?" "Sign out" "Signing out…" - "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." - "Recovery not set up" + "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages." + "You have turned off backup" + "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out." + "Your keys are still being backed up" + "Please wait for this to complete before signing out." + "Your keys are still being backed up" + "You are about to sign out of your last session. If you sign out now, you\'ll lose access to your encrypted messages." + "Recovery not set up" + "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." + "Have you saved your recovery key?" "Sign out" "Sign out" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index bb285968c6..555ff574df 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -12,7 +12,8 @@ "Location" "Poll" "Text Formatting" - "Message history is currently unavailable in this room" + "Message history is currently unavailable." + "Message history is unavailable in this room. Verify this device to see your message history." "Could not retrieve user details" "Would you like to invite them back?" "You are alone in this chat" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index c70eceb21f..be27e21ff4 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -51,7 +51,7 @@ "No" "Not now" "OK" - "Open settings" + "Settings" "Open with" "Quick reply" "Quote" @@ -145,6 +145,7 @@ "Server URL" "Settings" "Shared location" + "Signing out" "Starting chat…" "Sticker" "Success" @@ -167,8 +168,11 @@ "Video" "Voice message" "Waiting…" + "Waiting for decryption key" "Are you sure you want to end this poll?" "Poll: %1$s" + "Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup." + "Confirm your recovery key" "Confirmation" "Warning" "Activities" @@ -268,6 +272,8 @@ If you proceed, some of your settings may change." "Enter…" "Recovery key confirmed" "Confirm your recovery key" + "Copied recovery key" + "Generating…" "Save recovery key" "Write down your recovery key somewhere safe or save it in a password manager." "Tap to copy recovery key" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 3371b80814..5144dc191d 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -158,7 +158,8 @@ { "name": ":features:lockscreen:impl", "includeRegex": [ - "screen_app_lock_.*" + "screen_app_lock_.*", + "screen_signout_in_progress_dialog_content" ] } ] From 2cb0addd3e01b00c21e2bb7b8447a5ab4e1e262e Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 11:27:19 +0200 Subject: [PATCH 164/281] PIN: add callback on LockScreenEntryPoint --- .../features/ftue/impl/FtueFlowNode.kt | 6 +++++ .../lockscreen/api/LockScreenEntryPoint.kt | 6 +++++ .../impl/DefaultLockScreenEntryPoint.kt | 10 +++++++- .../lockscreen/impl/LockScreenFlowNode.kt | 25 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) 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 a8206ed927..ab5d163e60 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 @@ -145,7 +145,13 @@ class FtueFlowNode @AssistedInject constructor( analyticsEntryPoint.createNode(this, buildContext) } NavTarget.LockScreenSetup -> { + val callback = object : LockScreenEntryPoint.Callback { + override fun onSetupCompleted() { + lifecycleScope.launch { moveToNextStep() } + } + } lockScreenEntryPoint.nodeBuilder(this, buildContext) + .callback(callback) .target(LockScreenEntryPoint.Target.Setup) .build() } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt index fce3706dce..f63757717e 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.lockscreen.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 LockScreenEntryPoint : FeatureEntryPoint { @@ -25,10 +26,15 @@ interface LockScreenEntryPoint : FeatureEntryPoint { fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder fun target(target: Target): NodeBuilder fun build(): Node } + interface Callback: Plugin { + fun onSetupCompleted() + } + enum class Target { Settings, Setup, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 289f11113a..67182e4fff 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt @@ -30,8 +30,15 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder { var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock + val callbacks = mutableListOf() return object : LockScreenEntryPoint.NodeBuilder { + + override fun callback(callback: LockScreenEntryPoint.Callback): LockScreenEntryPoint.NodeBuilder { + callbacks += callback + return this + } + override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder { innerTarget = target return this @@ -45,7 +52,8 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings } ) - return parentNode.createNode(buildContext, listOf(inputs)) + val plugins = listOf(inputs) + callbacks + return parentNode.createNode(buildContext, plugins) } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index 31a11d06d7..e10ca406fb 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -20,13 +20,17 @@ import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.setup.SetupPinNode import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode @@ -41,6 +45,7 @@ import kotlinx.parcelize.Parcelize class LockScreenFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val pinCodeManager: PinCodeManager, ) : BackstackNode( backstack = BackStack( initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget, @@ -65,6 +70,26 @@ class LockScreenFlowNode @AssistedInject constructor( data object Settings : NavTarget } + private val pinCodeManagerCallback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + plugins().forEach { + it.onSetupCompleted() + } + } + } + + override fun onBuilt() { + super.onBuilt() + lifecycle.subscribe( + onCreate = { + pinCodeManager.addCallback(pinCodeManagerCallback) + }, + onDestroy = { + pinCodeManager.removeCallback(pinCodeManagerCallback) + } + ) + } + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Unlock -> { From 49e2060961f965c5c93c9a76d5e9927599b038ef Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 15:07:05 +0200 Subject: [PATCH 165/281] PIN unlock : makes sure to load the pin size from storage --- .../android/appconfig/LockScreenConfig.kt | 2 +- .../impl/components/PinEntryTextField.kt | 10 ++-- .../impl/pin/DefaultPinCodeManager.kt | 9 ++- .../lockscreen/impl/pin/PinCodeManager.kt | 5 ++ .../impl/unlock/PinUnlockPresenter.kt | 56 ++++++++++++++----- .../lockscreen/impl/unlock/PinUnlockState.kt | 2 +- .../impl/unlock/PinUnlockStateProvider.kt | 2 +- .../lockscreen/impl/unlock/PinUnlockView.kt | 10 ++-- .../impl/src/main/res/values/localazy.xml | 5 +- 9 files changed, 73 insertions(+), 28 deletions(-) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index c809e0c10e..75cf8406ed 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -21,7 +21,7 @@ object LockScreenConfig { /** * Whether the PIN is mandatory or not. */ - const val IS_PIN_MANDATORY: Boolean = true + const val IS_PIN_MANDATORY: Boolean = false /** * Some PINs are blacklisted. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index 017a926b61..e4869e71d8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -20,7 +20,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -58,16 +59,17 @@ fun PinEntryTextField( ) } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun PinEntryRow( pinEntry: PinEntry, isSecured: Boolean, modifier: Modifier = Modifier, ) { - Row( + FlowRow( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { for (digit in pinEntry.digits) { PinDigitView(digit = digit, isSecured = isSecured) @@ -98,7 +100,7 @@ private fun PinDigitView( ) { if (digit is PinDigit.Filled) { - val text = if(isSecured) { + val text = if (isSecured) { "•" } else { digit.value.toString() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index b7b861199e..e5e2dc6106 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -23,8 +23,6 @@ import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.cryptography.api.SecretKeyProvider import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.sessionstorage.api.observer.SessionObserver -import kotlinx.coroutines.launch import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -52,6 +50,13 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } + override suspend fun getPinCodeSize(): Int { + val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return 0 + val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) + val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) + return decryptedPinCode.size + } + override suspend fun createPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 2f0d44d9f2..c214e533ab 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -57,6 +57,11 @@ interface PinCodeManager { */ suspend fun isPinCodeAvailable(): Boolean + /** + * @return the size of the saved pin code. + */ + suspend fun getPinCodeSize(): Int + /** * Creates a new encrypted pin code. * @param pinCode the clear pin code to create diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index e73650314b..19f075b5b5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -24,13 +24,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -44,10 +44,10 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { - var pinEntry by remember { - //TODO fetch size from db - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + val pinEntryState = remember { + mutableStateOf>(Async.Uninitialized) } + val pinEntry by pinEntryState var remainingAttempts by remember { mutableStateOf>(Async.Uninitialized) } @@ -62,11 +62,18 @@ class PinUnlockPresenter @Inject constructor( mutableStateOf>(Async.Uninitialized) } + LaunchedEffect(Unit) { + suspend { + val pinCodeSize = pinCodeManager.getPinCodeSize() + PinEntry.createEmpty(pinCodeSize) + }.runCatchingUpdatingState(pinEntryState) + } + LaunchedEffect(pinEntry) { if (pinEntry.isComplete()) { val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText()) if (!isVerified) { - pinEntry = pinEntry.clear() + pinEntryState.value = pinEntry.clear() showWrongPinTitle = true } } @@ -80,7 +87,7 @@ class PinUnlockPresenter @Inject constructor( fun handleEvents(event: PinUnlockEvents) { when (event) { is PinUnlockEvents.OnPinKeypadPressed -> { - pinEntry = pinEntry.process(event.pinKeypadModel) + pinEntryState.value = pinEntry.process(event.pinKeypadModel) } PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false @@ -103,17 +110,38 @@ class PinUnlockPresenter @Inject constructor( ) } + private fun Async.isComplete(): Boolean { + return dataOrNull()?.isComplete().orFalse() + } + + private fun Async.toText(): String { + return dataOrNull()?.toText() ?: "" + } + + private fun Async.clear(): Async { + return when (this) { + is Async.Success -> Async.Success(data.clear()) + else -> this + } + } + + private fun Async.process(pinKeypadModel: PinKeypadModel): Async { + return when (this) { + is Async.Success -> { + val pinEntry = when (pinKeypadModel) { + PinKeypadModel.Back -> data.deleteLast() + is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number) + PinKeypadModel.Empty -> data + } + Async.Success(pinEntry) + } + else -> this + } + } + private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { matrixClient.logout() }.runCatchingUpdatingState(signOutAction) } - - private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry { - return when (pinKeypadModel) { - PinKeypadModel.Back -> deleteLast() - is PinKeypadModel.Number -> addDigit(pinKeypadModel.number) - PinKeypadModel.Empty -> this - } - } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index fa80254cce..178e8692d1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -20,7 +20,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.libraries.architecture.Async data class PinUnlockState( - val pinEntry: PinEntry, + val pinEntry: Async, val showWrongPinTitle: Boolean, val remainingAttempts: Async, val showSignOutPrompt: Boolean, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index cd2959dfe1..009a5bf4a4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -39,7 +39,7 @@ fun aPinUnlockState( showSignOutPrompt: Boolean = false, signOutAction: Async = Async.Uninitialized, ) = PinUnlockState( - pinEntry = pinEntry, + pinEntry = Async.Success(pinEntry), showWrongPinTitle = showWrongPinTitle, remainingAttempts = Async.Success(remainingAttempts), showSignOutPrompt = showSignOutPrompt, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 00a59e63cc..514531a718 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -258,7 +258,7 @@ private fun PinUnlockHeader( if (state.showWrongPinTitle) { pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts) } else { - stringResource(id = R.string.screen_app_lock_subtitle) + pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts) } } else { "" @@ -276,14 +276,16 @@ private fun PinUnlockHeader( color = subtitleColor, ) Spacer(Modifier.height(24.dp)) - PinDotsRow(state.pinEntry) + if (state.pinEntry is Async.Success) { + PinDotsRow(state.pinEntry.data) + } } } @Composable private fun PinUnlockFooter( - onUseBiometric: ()->Unit, - onForgotPin: ()->Unit, + onUseBiometric: () -> Unit, + onForgotPin: () -> Unit, modifier: Modifier = Modifier, ) { Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml index 529785b74d..bd6d522d57 100644 --- a/features/lockscreen/impl/src/main/res/values/localazy.xml +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -1,5 +1,9 @@ + + "You have %1$d attempt to unlock" + "You have %1$d attempts to unlock" + "Wrong PIN. You have %1$d more chance" "Wrong PIN. You have %1$d more chances" @@ -26,6 +30,5 @@ Choose something memorable. If you forget this PIN, you will be logged out of th "PINs don\'t match" "You’ll need to re-login and create a new PIN to proceed" "You are being signed out" - "You have 3 attempts to unlock" "Signing out…" From adf21e0e02538b4290a2e47028c9a87cb6d0b0e3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 16:13:30 +0200 Subject: [PATCH 166/281] PIN: fix and add tests --- .../impl/unlock/PinUnlockPresenter.kt | 7 +- .../lockscreen/impl/pin/PinCodeManager.kt | 28 +++++ .../impl/setup/SetupPinPresenterTest.kt | 19 +++- .../impl/unlock/PinUnlockPresenterTest.kt | 105 +++++++++++++++--- 4 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 19f075b5b5..b26f967b4c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -57,7 +57,6 @@ class PinUnlockPresenter @Inject constructor( var showSignOutPrompt by rememberSaveable { mutableStateOf(false) } - val signOutAction = remember { mutableStateOf>(Async.Uninitialized) } @@ -92,8 +91,10 @@ class PinUnlockPresenter @Inject constructor( PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false PinUnlockEvents.SignOut -> { - showSignOutPrompt = false - coroutineScope.signOut(signOutAction) + if (showSignOutPrompt) { + showSignOutPrompt = false + coroutineScope.signOut(signOutAction) + } } PinUnlockEvents.OnUseBiometric -> { //TODO diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt new file mode 100644 index 0000000000..a2e2dacf97 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -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. + */ + +package io.element.android.features.lockscreen.impl.pin + +import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore +import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService +import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider + +internal fun createPinCodeManager(): PinCodeManager { + val pinCodeStore = InMemoryPinCodeStore() + val secretKeyProvider = SimpleSecretKeyProvider() + val encryptionDecryptionService = AESEncryptionDecryptionService() + return DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index ff797b52f4..0cc38dd355 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,12 +20,15 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.features.lockscreen.impl.pin.createPinCodeManager import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import org.junit.Test @@ -38,8 +41,13 @@ class SetupPinPresenterTest { @Test fun `present - complete flow`() = runTest { - - val presenter = createSetupPinPresenter() + val pinCodeCreated = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeCreated.complete(Unit) + } + } + val presenter = createSetupPinPresenter(callback) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -96,10 +104,13 @@ class SetupPinPresenterTest { state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertText(completePin) } + pinCodeCreated.await() } } - private fun createSetupPinPresenter(): SetupPinPresenter { - return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta()) + private fun createSetupPinPresenter(callback: PinCodeManager.Callback): SetupPinPresenter { + val pinCodeManager = createPinCodeManager() + pinCodeManager.addCallback(callback) + return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta(), pinCodeManager) } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index 391a51e692..16e44b2af0 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -20,13 +20,16 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.pin.model.assertEmpty +import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.features.lockscreen.impl.pin.createPinCodeManager +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText -import io.element.android.features.lockscreen.impl.DefaultLockScreenService import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,16 +40,27 @@ class PinUnlockPresenterTest { private val completePin = "1235" @Test - fun `present - complete flow`() = runTest { - val presenter = createPinUnlockPresenter(this) + fun `present - success verify flow`() = runTest { + val pinCodeVerified = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeVerified.complete(Unit) + } + } + val presenter = createPinUnlockPresenter(this, callback) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().also { state -> - state.pinEntry.assertEmpty() + assertThat(state.pinEntry).isInstanceOf(Async.Uninitialized::class.java) assertThat(state.showWrongPinTitle).isFalse() assertThat(state.showSignOutPrompt).isFalse() - assertThat(state.remainingAttempts).isEqualTo(3) + assertThat(state.signOutAction).isInstanceOf(Async.Uninitialized::class.java) + assertThat(state.remainingAttempts).isInstanceOf(Async.Uninitialized::class.java) + } + consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last().also { state -> state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) } @@ -55,9 +69,55 @@ class PinUnlockPresenterTest { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty)) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) } awaitLastSequentialItem().also { state -> - state.pinEntry.assertText(halfCompletePin) + state.pinEntry.assertText(completePin) + } + pinCodeVerified.await() + } + } + + @Test + fun `present - failure verify flow`() = runTest { + val pinCodeVerified = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeVerified.complete(Unit) + } + } + val presenter = createPinUnlockPresenter(this, callback) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last() + val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0 + repeat(numberOfAttempts) { + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('4'))) + } + awaitLastSequentialItem().also { state -> + assertThat(state.remainingAttempts.dataOrNull()).isEqualTo(0) + assertThat(state.showSignOutPrompt).isEqualTo(true) + assertThat(state.isSignOutPromptCancellable).isEqualTo(false) + } + } + } + + @Test + fun `present - forgot pin flow`() = runTest { + val presenter = createPinUnlockPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last().also { state -> state.eventSink(PinUnlockEvents.OnForgetPin) } awaitLastSequentialItem().also { state -> @@ -67,22 +127,33 @@ class PinUnlockPresenterTest { } awaitLastSequentialItem().also { state -> assertThat(state.showSignOutPrompt).isEqualTo(false) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) + state.eventSink(PinUnlockEvents.OnForgetPin) } awaitLastSequentialItem().also { state -> - state.pinEntry.assertText(completePin) + assertThat(state.showSignOutPrompt).isEqualTo(true) + state.eventSink(PinUnlockEvents.SignOut) + } + consumeItemsUntilPredicate { state -> + state.signOutAction is Async.Success } } } - private suspend fun createPinUnlockPresenter(scope: CoroutineScope): PinUnlockPresenter { - val featureFlagService = FakeFeatureFlagService().apply { - setFeatureEnabled(FeatureFlags.PinUnlock, true) + private fun Async.assertText(text: String) { + dataOrNull()?.assertText(text) + } + + private suspend fun createPinUnlockPresenter( + scope: CoroutineScope, + callback: PinCodeManager.Callback = object : PinCodeManager.Callback {}, + ): PinUnlockPresenter { + val pinCodeManager = createPinCodeManager().apply { + addCallback(callback) + createPinCode(completePin) } - val lockScreenStateService = DefaultLockScreenService(featureFlagService) return PinUnlockPresenter( - lockScreenStateService, + pinCodeManager, + FakeMatrixClient(), scope, ) } From 626a884e1ffc77759d90eb73b75a02e16d781804 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 16:45:42 +0200 Subject: [PATCH 167/281] PIN: clean pin code storage --- features/lockscreen/impl/build.gradle.kts | 1 + .../impl/pin/storage/PinCodeStore.kt | 17 +-- .../pin/storage/PreferencesPinCodeStore.kt | 89 +++++++++++++++ .../storage/SharedPreferencesPinCodeStore.kt | 101 ------------------ .../impl/pin/storage/InMemoryPinCodeStore.kt | 12 +-- 5 files changed, 93 insertions(+), 127 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt delete mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 05e9ce20f3..fdb9dcd178 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) implementation(projects.services.appnavstate.api) + implementation(libs.androidx.datastore.preferences) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt index e72cbca2db..818bc6a47d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt @@ -18,10 +18,6 @@ package io.element.android.features.lockscreen.impl.pin.storage interface PinCodeStore : EncryptedPinCodeStorage { - interface Listener { - fun onPinSetUpChange(isConfigured: Boolean) - } - /** * Returns the remaining PIN code attempts. When this reaches 0 the PIN code access won't be available for some time. */ @@ -29,24 +25,13 @@ interface PinCodeStore : EncryptedPinCodeStorage { /** * Should decrement the number of remaining PIN code attempts. - * @return The remaining attempts. */ - suspend fun onWrongPin(): Int + suspend fun onWrongPin() /** * Resets the counter of attempts for PIN code and biometric access. */ suspend fun resetCounter() - - /** - * Adds a listener to be notified when the PIN code us created or removed. - */ - fun addListener(listener: Listener) - - /** - * Removes a listener to be notified when the PIN code us created or removed. - */ - fun removeListener(listener: Listener) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt new file mode 100644 index 0000000000..2bf9225a2c --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt @@ -0,0 +1,89 @@ +/* + * 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.lockscreen.impl.pin.storage + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.LockScreenConfig +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "pin_code_store") + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class PreferencesPinCodeStore @Inject constructor( + @ApplicationContext private val context: Context, +) : PinCodeStore { + + private val pinCodeKey = stringPreferencesKey("encoded_pin_code") + private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts") + + override suspend fun getRemainingPinCodeAttemptsNumber(): Int { + return context.dataStore.data.map { preferences -> + preferences[remainingAttemptsKey] ?: 0 + }.first() + } + + override suspend fun onWrongPin() { + context.dataStore.edit { preferences -> + val current = preferences[remainingAttemptsKey] ?: 0 + val remaining = (current - 1).coerceAtLeast(0) + preferences[remainingAttemptsKey] = remaining + } + } + + override suspend fun resetCounter() { + context.dataStore.edit { preferences -> + preferences[remainingAttemptsKey] = LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + } + } + + override suspend fun getEncryptedCode(): String? { + return context.dataStore.data.map { preferences -> + preferences[pinCodeKey] + }.first() + } + + override suspend fun saveEncryptedPinCode(pinCode: String) { + context.dataStore.edit { preferences -> + preferences[pinCodeKey] = pinCode + } + } + + override suspend fun deleteEncryptedPinCode() { + context.dataStore.edit { preferences -> + preferences.remove(pinCodeKey) + } + } + + override suspend fun hasPinCode(): Boolean { + return context.dataStore.data.map { preferences -> + preferences[pinCodeKey] != null + }.first() + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt deleted file mode 100644 index db84282ebc..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ /dev/null @@ -1,101 +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.lockscreen.impl.pin.storage - -import android.content.SharedPreferences -import androidx.core.content.edit -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appconfig.LockScreenConfig -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.DefaultPreferences -import io.element.android.libraries.di.SingleIn -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import java.util.concurrent.CopyOnWriteArrayList -import javax.inject.Inject - -private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY" -private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY" - -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class SharedPreferencesPinCodeStore @Inject constructor( - private val dispatchers: CoroutineDispatchers, - @DefaultPreferences private val sharedPreferences: SharedPreferences, -) : PinCodeStore { - - private val listeners = CopyOnWriteArrayList() - private val mutex = Mutex() - - override suspend fun getEncryptedCode(): String? = withContext(dispatchers.io) { - sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null) - } - - override suspend fun saveEncryptedPinCode(pinCode: String) = withContext(dispatchers.io) { - sharedPreferences.edit { - putString(ENCODED_PIN_CODE_KEY, pinCode) - } - withContext(dispatchers.main) { - listeners.forEach { it.onPinSetUpChange(isConfigured = true) } - } - } - - override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) { - sharedPreferences.edit { - remove(ENCODED_PIN_CODE_KEY) - } - withContext(dispatchers.main) { - listeners.forEach { it.onPinSetUpChange(isConfigured = false) } - } - } - - override suspend fun hasPinCode(): Boolean = withContext(dispatchers.io) { - sharedPreferences.contains(ENCODED_PIN_CODE_KEY) - } - - override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) { - sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) - } - - override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { - mutex.withLock { - val remaining = (getRemainingPinCodeAttemptsNumber() - 1).coerceAtLeast(0) - sharedPreferences.edit { - putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining) - } - remaining - } - } - - override suspend fun resetCounter() = withContext(dispatchers.io) { - mutex.withLock { - sharedPreferences.edit { - remove(REMAINING_PIN_CODE_ATTEMPTS_KEY) - } - } - } - - override fun addListener(listener: PinCodeStore.Listener) { - listeners.add(listener) - } - - override fun removeListener(listener: PinCodeStore.Listener) { - listeners.remove(listener) - } -} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt index 0b7c2f256b..6b6597728c 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt @@ -27,22 +27,14 @@ class InMemoryPinCodeStore : PinCodeStore { return remainingAttempts } - override suspend fun onWrongPin(): Int { - return remainingAttempts-- + override suspend fun onWrongPin() { + remainingAttempts-- } override suspend fun resetCounter() { remainingAttempts = DEFAULT_REMAINING_ATTEMPTS } - override fun addListener(listener: PinCodeStore.Listener) { - // no-op - } - - override fun removeListener(listener: PinCodeStore.Listener) { - // no-op - } - override suspend fun getEncryptedCode(): String? { return pinCode } From 3c5bff092721f2fda16e05bf917a38e604938295 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 16:54:34 +0200 Subject: [PATCH 168/281] Pin : clean remaining pin code attempts --- .../lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt index 2bf9225a2c..04f849e117 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt @@ -45,13 +45,13 @@ class PreferencesPinCodeStore @Inject constructor( override suspend fun getRemainingPinCodeAttemptsNumber(): Int { return context.dataStore.data.map { preferences -> - preferences[remainingAttemptsKey] ?: 0 + preferences.getRemainingPinCodeAttemptsNumber() }.first() } override suspend fun onWrongPin() { context.dataStore.edit { preferences -> - val current = preferences[remainingAttemptsKey] ?: 0 + val current = preferences.getRemainingPinCodeAttemptsNumber() val remaining = (current - 1).coerceAtLeast(0) preferences[remainingAttemptsKey] = remaining } @@ -86,4 +86,6 @@ class PreferencesPinCodeStore @Inject constructor( preferences[pinCodeKey] != null }.first() } + + private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT } From 9cf17dfa12fbe30c747f52c08298490f4e3e0c9e Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Wed, 25 Oct 2023 17:33:50 +0200 Subject: [PATCH 169/281] Voice message bubble design improvements (#1637) - Fixes color and size of both text and icons. --- .../components/event/TimelineItemVoiceView.kt | 45 +++++++++---------- .../timeline/VoiceMessageStateProvider.kt | 10 ++--- ...iceView-D-39_39_null_0,NEXUS_5,1.0,en].png | 3 -- ...iceView-D-39_39_null_1,NEXUS_5,1.0,en].png | 3 -- ...iceView-D-39_39_null_2,NEXUS_5,1.0,en].png | 3 -- ...iceView-D-40_40_null_0,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_1,NEXUS_5,1.0,en].png | 3 ++ ...ceView-D-40_40_null_10,NEXUS_5,1.0,en].png | 3 ++ ...ceView-D-40_40_null_11,NEXUS_5,1.0,en].png | 3 ++ ...ceView-D-40_40_null_12,NEXUS_5,1.0,en].png | 3 ++ ...ceView-D-40_40_null_13,NEXUS_5,1.0,en].png | 3 ++ ...ceView-D-40_40_null_14,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_2,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_3,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_4,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_5,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_6,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_7,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_8,NEXUS_5,1.0,en].png | 3 ++ ...iceView-D-40_40_null_9,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-39_40_null_0,NEXUS_5,1.0,en].png | 3 -- ...iceView-N-39_40_null_1,NEXUS_5,1.0,en].png | 3 -- ...iceView-N-39_40_null_2,NEXUS_5,1.0,en].png | 3 -- ...iceView-N-40_41_null_0,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_1,NEXUS_5,1.0,en].png | 3 ++ ...ceView-N-40_41_null_10,NEXUS_5,1.0,en].png | 3 ++ ...ceView-N-40_41_null_11,NEXUS_5,1.0,en].png | 3 ++ ...ceView-N-40_41_null_12,NEXUS_5,1.0,en].png | 3 ++ ...ceView-N-40_41_null_13,NEXUS_5,1.0,en].png | 3 ++ ...ceView-N-40_41_null_14,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_2,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_3,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_4,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_5,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_6,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_7,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_8,NEXUS_5,1.0,en].png | 3 ++ ...iceView-N-40_41_null_9,NEXUS_5,1.0,en].png | 3 ++ ...ewUnified-D-40_40_null,NEXUS_5,1.0,en].png | 3 -- ...ewUnified-D-41_41_null,NEXUS_5,1.0,en].png | 3 ++ ...ewUnified-N-40_41_null,NEXUS_5,1.0,en].png | 3 -- ...ewUnified-N-41_42_null,NEXUS_5,1.0,en].png | 3 ++ ...eaderView-D-41_41_null,NEXUS_5,1.0,en].png | 3 -- ...aderView-D-42_42_null,NEXUS_5,1.0,en].png} | 0 ...eaderView-N-41_42_null,NEXUS_5,1.0,en].png | 3 -- ...aderView-N-42_43_null,NEXUS_5,1.0,en].png} | 0 ...ocument-D-42_42_null_0,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_1,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_10,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_11,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_12,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_13,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_14,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_15,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_16,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_17,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_18,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_19,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_2,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_20,NEXUS_5,1.0,en].png | 3 -- ...cument-D-42_42_null_21,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_3,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_4,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_5,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_6,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_7,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_8,NEXUS_5,1.0,en].png | 3 -- ...ocument-D-42_42_null_9,NEXUS_5,1.0,en].png | 3 -- ...cument-D-43_43_null_0,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_1,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_10,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_11,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_12,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_13,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_14,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_15,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_16,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_17,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_18,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_19,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_2,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_20,NEXUS_5,1.0,en].png} | 0 ...ument-D-43_43_null_21,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_3,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_4,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_5,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_6,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_7,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_8,NEXUS_5,1.0,en].png} | 0 ...cument-D-43_43_null_9,NEXUS_5,1.0,en].png} | 0 ...ocument-N-42_43_null_0,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_1,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_10,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_11,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_12,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_13,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_14,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_15,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_16,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_17,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_18,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_19,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_2,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_20,NEXUS_5,1.0,en].png | 3 -- ...cument-N-42_43_null_21,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_3,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_4,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_5,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_6,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_7,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_8,NEXUS_5,1.0,en].png | 3 -- ...ocument-N-42_43_null_9,NEXUS_5,1.0,en].png | 3 -- ...cument-N-43_44_null_0,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_1,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_10,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_11,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_12,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_13,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_14,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_15,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_16,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_17,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_18,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_19,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_2,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_20,NEXUS_5,1.0,en].png} | 0 ...ument-N-43_44_null_21,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_3,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_4,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_5,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_6,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_7,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_8,NEXUS_5,1.0,en].png} | 0 ...cument-N-43_44_null_9,NEXUS_5,1.0,en].png} | 0 ...Content-D-43_43_null_0,NEXUS_5,1.0,en].png | 3 -- ...ontent-D-44_44_null_0,NEXUS_5,1.0,en].png} | 0 ...Content-N-43_44_null_0,NEXUS_5,1.0,en].png | 3 -- ...ontent-N-44_45_null_0,NEXUS_5,1.0,en].png} | 0 ...ageMenu-D-44_44_null_0,NEXUS_5,1.0,en].png | 3 -- ...ageMenu-D-44_44_null_1,NEXUS_5,1.0,en].png | 3 -- ...geMenu-D-45_45_null_0,NEXUS_5,1.0,en].png} | 0 ...geMenu-D-45_45_null_1,NEXUS_5,1.0,en].png} | 0 ...ageMenu-N-44_45_null_0,NEXUS_5,1.0,en].png | 3 -- ...ageMenu-N-44_45_null_1,NEXUS_5,1.0,en].png | 3 -- ...geMenu-N-45_46_null_0,NEXUS_5,1.0,en].png} | 0 ...geMenu-N-45_46_null_1,NEXUS_5,1.0,en].png} | 0 ...annerView-D-45_45_null,NEXUS_5,1.0,en].png | 3 -- ...nnerView-D-46_46_null,NEXUS_5,1.0,en].png} | 0 ...annerView-N-45_46_null,NEXUS_5,1.0,en].png | 3 -- ...nnerView-N-46_47_null,NEXUS_5,1.0,en].png} | 0 ...torView-D-46_46_null_0,NEXUS_5,1.0,en].png | 3 -- ...torView-D-46_46_null_1,NEXUS_5,1.0,en].png | 3 -- ...orView-D-47_47_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-D-47_47_null_1,NEXUS_5,1.0,en].png} | 0 ...torView-N-46_47_null_0,NEXUS_5,1.0,en].png | 3 -- ...torView-N-46_47_null_1,NEXUS_5,1.0,en].png | 3 -- ...orView-N-47_48_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-N-47_48_null_1,NEXUS_5,1.0,en].png} | 0 ...Indicator-D-47_47_null,NEXUS_5,1.0,en].png | 3 -- ...ndicator-D-48_48_null,NEXUS_5,1.0,en].png} | 0 ...Indicator-N-47_48_null,NEXUS_5,1.0,en].png | 3 -- ...ndicator-N-48_49_null,NEXUS_5,1.0,en].png} | 0 ...gInfoView-D-48_48_null,NEXUS_5,1.0,en].png | 3 -- ...InfoView-D-49_49_null,NEXUS_5,1.0,en].png} | 0 ...gInfoView-N-48_49_null,NEXUS_5,1.0,en].png | 3 -- ...InfoView-N-49_50_null,NEXUS_5,1.0,en].png} | 0 ...ndicator-D-50_50_null,NEXUS_5,1.0,en].png} | 0 ...ndicator-N-50_51_null,NEXUS_5,1.0,en].png} | 0 168 files changed, 122 insertions(+), 239 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-39_39_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-39_39_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-39_39_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-39_40_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-39_40_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-39_40_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-40_40_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-40_41_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-41_41_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-40_40_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-42_42_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-41_42_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-40_41_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-42_43_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_10,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_11,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_12,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_13,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_14,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_15,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_16,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_17,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_18,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_19,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_20,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_21,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_4,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_5,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_6,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_7,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_8,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-42_42_null_9,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_16,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_16,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_17,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_17,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_18,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_18,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_19,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_19,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_20,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_20,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_21,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_21,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-41_41_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_9,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_10,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_11,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_12,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_13,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_14,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_15,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_16,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_17,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_18,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_19,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_20,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_21,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_4,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_5,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_6,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_7,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_8,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-42_43_null_9,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_16,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_16,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_17,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_17,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_18,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_18,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_19,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_19,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_20,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_20,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_21,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_21,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-41_42_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_9,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-43_43_null_0,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-42_42_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-44_44_null_0,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-43_44_null_0,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-42_43_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-44_45_null_0,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-44_44_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-44_44_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-43_43_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-45_45_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-43_43_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-45_45_null_1,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-44_45_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-44_45_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-43_44_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-45_46_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-43_44_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-45_46_null_1,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-45_45_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-44_44_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-45_46_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-44_45_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-46_46_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-46_46_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-45_45_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-47_47_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-45_45_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-47_47_null_1,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-46_47_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-46_47_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-45_46_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-47_48_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-45_46_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-47_48_null_1,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-47_47_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-46_46_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-48_48_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-47_48_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-46_47_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-48_49_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-48_48_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-47_47_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-49_49_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-48_49_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-47_48_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-49_50_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-D-49_49_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-D-50_50_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-N-49_50_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-N-50_51_null,NEXUS_5,1.0,en].png} (100%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index e30993fe5b..bbcebd006d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -68,26 +68,18 @@ fun TimelineItemVoiceView( modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { - Box( - modifier = Modifier - .size(32.dp) - .clip(CircleShape) - .background(ElementTheme.materialColors.background), - contentAlignment = Alignment.Center, - ) { - when (state.button) { - VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause) - VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause) - VoiceMessageState.Button.Downloading -> ProgressButton() - VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause) - VoiceMessageState.Button.Disabled -> DisabledPlayButton() - } + when (state.button) { + VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause) + VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause) + VoiceMessageState.Button.Downloading -> ProgressButton() + VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause) + VoiceMessageState.Button.Disabled -> DisabledPlayButton() } Spacer(Modifier.width(8.dp)) Text( text = state.time, color = ElementTheme.materialColors.secondary, - style = ElementTheme.typography.fontBodySmRegular, + style = ElementTheme.typography.fontBodySmMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -143,9 +135,9 @@ private fun ProgressButton() { CircularProgressIndicator( modifier = Modifier .padding(2.dp) - .size(12.dp), - color = ElementTheme.materialColors.primary, - strokeWidth = 1.6.dp, + .size(16.dp), + color = ElementTheme.colors.iconSecondary, + strokeWidth = 2.dp, ) } } @@ -171,9 +163,8 @@ private fun IconButton( Icon( painter = painterResource(id = drawableRes), contentDescription = contentDescription, - tint = ElementTheme.materialColors.primary, - modifier = Modifier - .size(16.dp), + tint = ElementTheme.colors.iconSecondary, + modifier = Modifier.size(24.dp), ) } } @@ -185,7 +176,7 @@ private fun Button( ) { Box( modifier = Modifier - .size(32.dp) + .size(36.dp) .clip(CircleShape) .background(ElementTheme.materialColors.background) .let { @@ -201,8 +192,14 @@ open class TimelineItemVoiceViewParametersProvider : PreviewParameterProvider - get() = voiceMessageStateProvider.values.zip(timelineItemVoiceContentProvider.values) - .map { TimelineItemVoiceViewParameters(it.first, it.second) } + get() = timelineItemVoiceContentProvider.values.flatMap { content -> + voiceMessageStateProvider.values.map { state -> + TimelineItemVoiceViewParameters( + state = state, + content = content, + ) + } + } } data class TimelineItemVoiceViewParameters( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt index ca40ec8cd7..e09c0a0449 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt @@ -24,31 +24,31 @@ open class VoiceMessageStateProvider : PreviewParameterProvider Date: Wed, 25 Oct 2023 18:37:02 +0200 Subject: [PATCH 170/281] Pin: add tests and make LockScreenConfig an injectable data class --- app/build.gradle.kts | 1 + appconfig/build.gradle.kts | 11 +++ .../android/appconfig/LockScreenConfig.kt | 34 ++++++-- .../impl/DefaultLockScreenService.kt | 5 +- .../pin/storage/PreferencesPinCodeStore.kt | 5 +- .../impl/settings/LockScreenSettingsEvents.kt | 2 +- .../settings/LockScreenSettingsPresenter.kt | 14 ++-- .../impl/settings/LockScreenSettingsView.kt | 2 +- .../impl/setup/SetupPinPresenter.kt | 5 +- .../impl/setup/validation/PinValidator.kt | 7 +- .../impl/fixtures/LockScreenConfig.kt | 35 ++++++++ .../impl/{pin => fixtures}/PinCodeManager.kt | 15 ++-- .../LockScreenSettingsPresenterTest.kt | 79 +++++++++++++++++++ .../impl/setup/SetupPinPresenterTest.kt | 20 ++++- .../impl/unlock/PinUnlockPresenterTest.kt | 4 +- 15 files changed, 203 insertions(+), 36 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/{pin => fixtures}/PinCodeManager.kt (59%) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ac84cfec2..324da8447d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -201,6 +201,7 @@ dependencies { implementation(projects.features.call) implementation(projects.anvilannotations) implementation(projects.appnav) + implementation(projects.appconfig) anvil(projects.anvilcodegen) implementation(libs.appyx.core) diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 3c03739553..3f9275d383 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -16,9 +16,20 @@ plugins { id("java-library") alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.di) +} diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index 75cf8406ed..e73b3cd150 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -16,30 +16,52 @@ package io.element.android.appconfig -object LockScreenConfig { +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.AppScope +/** + * Configuration for the lock screen feature. + */ +data class LockScreenConfig( /** * Whether the PIN is mandatory or not. */ - const val IS_PIN_MANDATORY: Boolean = false + val isPinMandatory: Boolean, /** * Some PINs are blacklisted. */ - val PIN_BLACKLIST = setOf("0000", "1234") + val pinBlacklist: Set, /** * The size of the PIN. */ - const val PIN_SIZE = 4 + val pinSize: Int, /** * Number of attempts before the user is logged out. */ - const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 + val maxPinCodeAttemptsBeforeLogout: Int, /** * Time period before locking the app once backgrounded. */ - const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L + val gracePeriodInMillis: Long +) + +@ContributesTo(AppScope::class) +@Module +object LockScreenConfigModule { + + @Provides + fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig( + isPinMandatory = false, + pinBlacklist = setOf("0000", "1234"), + pinSize = 4, + maxPinCodeAttemptsBeforeLogout = 3, + gracePeriodInMillis = 90_000L + ) + } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt index afdc4e7899..32e1e3d1b9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -39,6 +39,7 @@ import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultLockScreenService @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val featureFlagService: FeatureFlagService, private val pinCodeManager: PinCodeManager, private val coroutineScope: CoroutineScope, @@ -91,14 +92,14 @@ class DefaultLockScreenService @Inject constructor( if (isInForeground) { lockJob?.cancel() } else { - lockJob = lockIfNeeded(delayInMillis = LockScreenConfig.GRACE_PERIOD_IN_MILLIS) + lockJob = lockIfNeeded(delayInMillis = lockScreenConfig.gracePeriodInMillis) } } } } override suspend fun isSetupRequired(): Boolean { - return LockScreenConfig.IS_PIN_MANDATORY + return lockScreenConfig.isPinMandatory && featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && !pinCodeManager.isPinCodeAvailable() } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt index 04f849e117..8631c05502 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt @@ -38,6 +38,7 @@ private val Context.dataStore: DataStore by preferencesDataStore(na @ContributesBinding(AppScope::class) class PreferencesPinCodeStore @Inject constructor( @ApplicationContext private val context: Context, + private val lockScreenConfig: LockScreenConfig, ) : PinCodeStore { private val pinCodeKey = stringPreferencesKey("encoded_pin_code") @@ -59,7 +60,7 @@ class PreferencesPinCodeStore @Inject constructor( override suspend fun resetCounter() { context.dataStore.edit { preferences -> - preferences[remainingAttemptsKey] = LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout } } @@ -87,5 +88,5 @@ class PreferencesPinCodeStore @Inject constructor( }.first() } - private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt index 110ca542de..9032e8d0ef 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt @@ -17,7 +17,7 @@ package io.element.android.features.lockscreen.impl.settings sealed interface LockScreenSettingsEvents { - data object RemovePin : LockScreenSettingsEvents + data object OnRemovePin : LockScreenSettingsEvents data object ConfirmRemovePin : LockScreenSettingsEvents data object CancelRemovePin : LockScreenSettingsEvents data object ToggleBiometric : LockScreenSettingsEvents diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt index a91597448a..9bef944194 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject class LockScreenSettingsPresenter @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val pinCodeManager: PinCodeManager, private val coroutineScope: CoroutineScope, ) : Presenter { @@ -50,7 +51,7 @@ class LockScreenSettingsPresenter @Inject constructor( mutableStateOf(false) } LaunchedEffect(triggerComputation) { - showRemovePinOption = !LockScreenConfig.IS_PIN_MANDATORY && pinCodeManager.isPinCodeAvailable() + showRemovePinOption = !lockScreenConfig.isPinMandatory && pinCodeManager.isPinCodeAvailable() } fun handleEvents(event: LockScreenSettingsEvents) { @@ -58,12 +59,14 @@ class LockScreenSettingsPresenter @Inject constructor( LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false LockScreenSettingsEvents.ConfirmRemovePin -> { coroutineScope.launch { - showRemovePinConfirmation = false - pinCodeManager.deletePinCode() - triggerComputation++ + if (showRemovePinConfirmation) { + showRemovePinConfirmation = false + pinCodeManager.deletePinCode() + triggerComputation++ + } } } - LockScreenSettingsEvents.RemovePin -> showRemovePinConfirmation = true + LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true LockScreenSettingsEvents.ToggleBiometric -> { //TODO branch biometric logic } @@ -77,5 +80,4 @@ class LockScreenSettingsPresenter @Inject constructor( eventSink = ::handleEvents ) } - } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index 08234b6b07..97c640ba78 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -54,7 +54,7 @@ fun LockScreenSettingsView( title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), tintColor = ElementTheme.colors.textCriticalPrimary, onClick = { - state.eventSink(LockScreenSettingsEvents.RemovePin) + state.eventSink(LockScreenSettingsEvents.OnRemovePin) } ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 06de31281c..89e8b98c51 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.delay import javax.inject.Inject class SetupPinPresenter @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val pinValidator: PinValidator, private val buildMeta: BuildMeta, private val pinCodeManager: PinCodeManager, @@ -41,10 +42,10 @@ class SetupPinPresenter @Inject constructor( @Composable override fun present(): SetupPinState { var choosePinEntry by remember { - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(lockScreenConfig.pinSize)) } var confirmPinEntry by remember { - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(lockScreenConfig.pinSize)) } var isConfirmationStep by remember { mutableStateOf(false) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index b164ee8c88..ec17411396 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -20,10 +20,7 @@ import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject -class PinValidator internal constructor(private val pinBlacklist: Set) { - - @Inject - constructor() : this(LockScreenConfig.PIN_BLACKLIST) +class PinValidator @Inject constructor(private val lockScreenConfig: LockScreenConfig) { sealed interface Result { data object Valid : Result @@ -32,7 +29,7 @@ class PinValidator internal constructor(private val pinBlacklist: Set) { fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() - val isBlacklisted = pinBlacklist.any { it == pinAsText } + val isBlacklisted = lockScreenConfig.pinBlacklist.any { it == pinAsText } return if (isBlacklisted) { Result.Invalid(SetupPinFailure.PinBlacklisted) } else { diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt new file mode 100644 index 0000000000..d42dad101a --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.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.features.lockscreen.impl.fixtures + +import io.element.android.appconfig.LockScreenConfig + +internal fun aLockScreenConfig( + isPinMandatory: Boolean = false, + pinBlacklist: Set = emptySet(), + pinSize: Int = 4, + maxPinCodeAttemptsBeforeLogout: Int = 3, + gracePeriodInMillis: Long = 5 * 60 * 1000L +): LockScreenConfig { + return LockScreenConfig( + isPinMandatory = isPinMandatory, + pinBlacklist = pinBlacklist, + pinSize = pinSize, + maxPinCodeAttemptsBeforeLogout = maxPinCodeAttemptsBeforeLogout, + gracePeriodInMillis = gracePeriodInMillis + ) +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt similarity index 59% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt index a2e2dacf97..bf9ebdf541 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt @@ -14,15 +14,20 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.pin +package io.element.android.features.lockscreen.impl.fixtures +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManager +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore +import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore +import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider -internal fun createPinCodeManager(): PinCodeManager { - val pinCodeStore = InMemoryPinCodeStore() - val secretKeyProvider = SimpleSecretKeyProvider() - val encryptionDecryptionService = AESEncryptionDecryptionService() +internal fun aPinCodeManager( + pinCodeStore: PinCodeStore = InMemoryPinCodeStore(), + secretKeyProvider: SimpleSecretKeyProvider = SimpleSecretKeyProvider(), + encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(), +): PinCodeManager { return DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt new file mode 100644 index 0000000000..578f20e7d9 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt @@ -0,0 +1,79 @@ +/* + * 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.lockscreen.impl.settings + +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.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager +import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LockScreenSettingsPresenterTest { + + @Test + fun `present - remove pin flow`() = runTest { + val presenter = createLockScreenSettingsPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilPredicate { state -> + state.showRemovePinOption + }.last().also { state -> + state.eventSink(LockScreenSettingsEvents.OnRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isTrue() + state.eventSink(LockScreenSettingsEvents.CancelRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isFalse() + state.eventSink(LockScreenSettingsEvents.OnRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isTrue() + state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) + } + consumeItemsUntilPredicate { + it.showRemovePinOption.not() + }.last().also { state -> + assertThat(state.showRemovePinConfirmation).isFalse() + assertThat(state.showRemovePinOption).isFalse() + } + } + } + + private suspend fun createLockScreenSettingsPresenter( + coroutineScope: CoroutineScope, + lockScreenConfig: LockScreenConfig = aLockScreenConfig(), + ): LockScreenSettingsPresenter { + val pinCodeManager = aPinCodeManager().apply { + createPinCode("1234") + } + return LockScreenSettingsPresenter( + pinCodeManager = pinCodeManager, + coroutineScope = coroutineScope, + lockScreenConfig = lockScreenConfig, + ) + } +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 0cc38dd355..81c37e6d69 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,8 +20,10 @@ 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.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager -import io.element.android.features.lockscreen.impl.pin.createPinCodeManager import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator @@ -108,9 +110,19 @@ class SetupPinPresenterTest { } } - private fun createSetupPinPresenter(callback: PinCodeManager.Callback): SetupPinPresenter { - val pinCodeManager = createPinCodeManager() + private fun createSetupPinPresenter( + callback: PinCodeManager.Callback, + lockScreenConfig: LockScreenConfig = aLockScreenConfig( + pinBlacklist = setOf(blacklistedPin) + ), + ): SetupPinPresenter { + val pinCodeManager = aPinCodeManager() pinCodeManager.addCallback(callback) - return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta(), pinCodeManager) + return SetupPinPresenter( + lockScreenConfig = lockScreenConfig, + pinValidator = PinValidator(lockScreenConfig), + buildMeta = aBuildMeta(), + pinCodeManager = pinCodeManager + ) } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index 16e44b2af0..e6ba51c537 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -21,7 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.pin.PinCodeManager -import io.element.android.features.lockscreen.impl.pin.createPinCodeManager +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel @@ -147,7 +147,7 @@ class PinUnlockPresenterTest { scope: CoroutineScope, callback: PinCodeManager.Callback = object : PinCodeManager.Callback {}, ): PinUnlockPresenter { - val pinCodeManager = createPinCodeManager().apply { + val pinCodeManager = aPinCodeManager().apply { addCallback(callback) createPinCode(completePin) } From 296cd7ca143d15b452dc96d13c8c86b864520bde Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 25 Oct 2023 22:12:01 +0100 Subject: [PATCH 171/281] Add custom waveform with cursor and nice gesture support. --- .../messages/impl/mediaplayer/MediaPlayer.kt | 3 + .../components/event/TimelineItemVoiceView.kt | 19 +- .../timeline/VoiceMessageStateProvider.kt | 8 +- .../timeline/WaveformPlaybackView.kt | 214 ++++++++++++++++++ 4 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/MediaPlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/MediaPlayer.kt index 71acc9bb3c..0055496f43 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/MediaPlayer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/MediaPlayer.kt @@ -174,6 +174,9 @@ class MediaPlayerImpl @Inject constructor( override fun seekTo(positionMs: Long) { player.seekTo(positionMs) + _state.update { + it.copy(currentPosition = player.currentPosition) + } } override fun close() { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index e30993fe5b..b0b287f9dc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -44,6 +44,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider +import io.element.android.features.messages.impl.voicemessages.timeline.Waveform +import io.element.android.features.messages.impl.voicemessages.timeline.WaveformPlaybackView import io.element.android.features.messages.impl.voicemessages.timeline.WaveformProgressIndicator import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -92,14 +94,23 @@ fun TimelineItemVoiceView( overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.width(8.dp)) - WaveformProgressIndicator( + WaveformPlaybackView( + showCursor = state.button == VoiceMessageState.Button.Pause, + playbackProgress = state.progress, + waveform = Waveform(data = content.waveform), modifier = Modifier .height(34.dp) .weight(1f), - progress = state.progress, - amplitudes = content.waveform, - onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) } + onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) }, ) +// WaveformProgressIndicator( +// modifier = Modifier +// .height(34.dp) +// .weight(1f), +// progress = state.progress, +// amplitudes = content.waveform, +// onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) } +// ) Spacer(Modifier.width(extraPadding.getDpSize())) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt index ca40ec8cd7..faa605382c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt @@ -35,7 +35,13 @@ open class VoiceMessageStateProvider : PreviewParameterProvider +) { + companion object { + private val dataRange = 0..1024 + } + + fun normalisedData(maxSamplesCount: Int): ImmutableList { + if(maxSamplesCount <= 0) { + return persistentListOf() + } + + // Filter the data to keep only the expected number of samples + val result = if (data.size > maxSamplesCount) { + (0.. + val targetIndex = (index.toDouble() * (data.count().toDouble() / maxSamplesCount.toDouble())).roundToInt() + data[targetIndex] + } + } else { + data + } + + // Normalize the sample in the allowed range + return result.map { it.toFloat() / dataRange.last.toFloat() }.toPersistentList() + } +} +private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun WaveformPlaybackView( + playbackProgress: Float, + showCursor: Boolean, + waveform: Waveform, + modifier: Modifier = Modifier, + onSeek: (progress: Float) -> Unit = {}, + brush: Brush = SolidColor(ElementTheme.colors.iconQuaternary), + progressBrush: Brush = SolidColor(ElementTheme.colors.iconSecondary), + cursorBrush: Brush = SolidColor(ElementTheme.colors.iconAccentTertiary), + lineWidth: Dp = 2.dp, + linePadding: Dp = 2.dp, + minimumGraphAmplitude: Float = 2F, +) { + var seekProgress = remember { mutableStateOf(null) } + var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) } + var canvasSizePx by remember { mutableStateOf(Size(0f, 0f)) } + val progress by remember(playbackProgress, seekProgress.value) { + derivedStateOf { + seekProgress.value ?: playbackProgress + } + } + val progressAnimated = animateFloatAsState(targetValue = progress, label = "progressAnimation") + val amplitudeDisplayCount by remember(canvasSize) { + derivedStateOf { + ((canvasSize.width.value) / (lineWidth.value + linePadding.value)).toInt() + } + } + val normalizedWaveformData by remember(amplitudeDisplayCount) { + derivedStateOf { + waveform.normalisedData(amplitudeDisplayCount) + } + } + + val requestDisallowInterceptTouchEvent = remember { RequestDisallowInterceptTouchEvent() } + Canvas( + modifier = Modifier + .fillMaxWidth() + .graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA) + .pointerInteropFilter(requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent) { + return@pointerInteropFilter when (it.action) { + MotionEvent.ACTION_DOWN -> { + if (it.x in 0F..canvasSizePx.width) { + requestDisallowInterceptTouchEvent.invoke(true) + seekProgress.value = (it.x / canvasSizePx.width) + true + } else false + } + MotionEvent.ACTION_MOVE -> { + if (it.x in 0F..canvasSizePx.width) { + seekProgress.value = (it.x / canvasSizePx.width) + } + true + } + MotionEvent.ACTION_UP -> { + requestDisallowInterceptTouchEvent.invoke(false) + seekProgress.value?.let(onSeek) + seekProgress.value = null + true + } + else -> false + } + } + .then(modifier) + ) { + canvasSize = size.toDpSize() + canvasSizePx = size + val centerY = canvasSize.height.toPx() / 2 + val cornerRadius = lineWidth / 2 + normalizedWaveformData.forEachIndexed { index, amplitude -> + val drawingAmplitude = max(minimumGraphAmplitude, amplitude * (canvasSize.height.toPx() - 2)) + drawRoundRect( + brush = brush, + topLeft = Offset( + x = index * (linePadding + lineWidth).toPx(), + y = centerY - (drawingAmplitude / 2) + ), + size = Size( + width = lineWidth.toPx(), + height = drawingAmplitude + ), + cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx()), + style = Fill + ) + } + drawRect( + brush = progressBrush, + size = Size( + width = (progressAnimated.value) * canvasSize.width.toPx(), + height = canvasSize.height.toPx() + ), + blendMode = BlendMode.SrcAtop + ) + if(showCursor || seekProgress.value != null) { + drawRoundRect( + brush = cursorBrush, + topLeft = Offset( + x = progressAnimated.value * canvasSize.width.toPx(), + y = centerY - ((canvasSize.height.toPx() - 2) / 2) + ), + size = Size( + width = lineWidth.toPx(), + height = canvasSize.height.toPx() - 2 + ), + cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx()), + style = Fill + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun WaveformPlaybackViewPreview() = ElementPreview { + Column { + WaveformPlaybackView( + showCursor = false, + playbackProgress = 0.5f, + waveform = Waveform(persistentListOf()), + ) + WaveformPlaybackView( + showCursor = false, + playbackProgress = 0.5f, + waveform = Waveform(persistentListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)), + ) + WaveformPlaybackView( + showCursor = true, + playbackProgress = 0.5f, + waveform = Waveform(List(1024) { it }.toPersistentList()), + ) + } +} From 71ef76ad867651e259c7f4ac601e21881fb8d0f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 08:59:06 +0200 Subject: [PATCH 172/281] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.65 (#1639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency org.matrix.rustcomponents:sdk-android to v0.1.65 * Fix broken verification flow --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín --- gradle/libs.versions.toml | 2 +- .../android/libraries/matrix/impl/RustMatrixClient.kt | 2 +- .../impl/verification/RustSessionVerificationService.kt | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9499d81ee..56e9f0cfbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -149,7 +149,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.64" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.65" 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" } 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 14cf3cd17e..21a489904b 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 @@ -99,7 +99,7 @@ class RustMatrixClient constructor( private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}") private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope) - private val verificationService = RustSessionVerificationService(rustSyncService) + private val verificationService = RustSessionVerificationService(rustSyncService, sessionCoroutineScope) private val pushersService = RustPushersService( client = client, dispatchers = dispatchers, 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 29797ed3c4..6616539e95 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 @@ -23,10 +23,12 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch import org.matrix.rustcomponents.sdk.SessionVerificationController import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface @@ -35,6 +37,7 @@ import javax.inject.Inject class RustSessionVerificationService @Inject constructor( private val syncService: RustSyncService, + private val sessionCoroutineScope: CoroutineScope, ) : SessionVerificationService, SessionVerificationControllerDelegate { var verificationController: SessionVerificationControllerInterface? = null @@ -44,7 +47,7 @@ class RustSessionVerificationService @Inject constructor( // If status was 'Unknown', move it to either 'Verified' or 'NotVerified' if (value != null) { value.setDelegate(this) - updateVerificationStatus(value.isVerified()) + sessionCoroutineScope.launch { updateVerificationStatus(value.isVerified()) } } } From 827edecda401c8649f4270b77594cd1a9ba3a4d5 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 26 Oct 2023 08:58:03 +0100 Subject: [PATCH 173/281] Remove WaveformProgressIndicator and waveform library and lint. -Remove WaveformProgressIndicator - Remove waveform library - lint. --- features/messages/impl/build.gradle.kts | 1 - .../components/event/TimelineItemVoiceView.kt | 1 - .../impl/voicemessages/timeline/Waveform.kt | 50 ++++++++++ .../timeline/WaveformPlaybackView.kt | 43 ++------- .../timeline/WaveformProgressIndicator.kt | 96 ------------------- gradle/libs.versions.toml | 1 - settings.gradle.kts | 1 - 7 files changed, 57 insertions(+), 136 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/Waveform.kt delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformProgressIndicator.kt diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index db625e5b74..814262321c 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -65,7 +65,6 @@ dependencies { implementation(libs.vanniktech.blurhash) implementation(libs.telephoto.zoomableimage) implementation(libs.matrix.emojibase.bindings) - implementation(libs.audiowaveform) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index b0b287f9dc..e6259e291c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -46,7 +46,6 @@ import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMes import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider import io.element.android.features.messages.impl.voicemessages.timeline.Waveform import io.element.android.features.messages.impl.voicemessages.timeline.WaveformPlaybackView -import io.element.android.features.messages.impl.voicemessages.timeline.WaveformProgressIndicator import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/Waveform.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/Waveform.kt new file mode 100644 index 0000000000..708aee8b8e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/Waveform.kt @@ -0,0 +1,50 @@ +/* + * 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.voicemessages.timeline + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlin.math.roundToInt + +data class Waveform ( + val data: ImmutableList +) { + companion object { + private val dataRange = 0..1024 + } + + fun normalisedData(maxSamplesCount: Int): ImmutableList { + if(maxSamplesCount <= 0) { + return persistentListOf() + } + + // Filter the data to keep only the expected number of samples + val result = if (data.size > maxSamplesCount) { + (0.. + val targetIndex = (index.toDouble() * (data.count().toDouble() / maxSamplesCount.toDouble())).roundToInt() + data[targetIndex] + } + } else { + data + } + + // Normalize the sample in the allowed range + return result.map { it.toFloat() / dataRange.last.toFloat() }.toPersistentList() + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt index eeaf34490d..4759a58ef9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt @@ -45,39 +45,10 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.theme.ElementTheme -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlin.math.max -import kotlin.math.roundToInt -data class Waveform ( - val data: ImmutableList -) { - companion object { - private val dataRange = 0..1024 - } - - fun normalisedData(maxSamplesCount: Int): ImmutableList { - if(maxSamplesCount <= 0) { - return persistentListOf() - } - - // Filter the data to keep only the expected number of samples - val result = if (data.size > maxSamplesCount) { - (0.. - val targetIndex = (index.toDouble() * (data.count().toDouble() / maxSamplesCount.toDouble())).roundToInt() - data[targetIndex] - } - } else { - data - } - - // Normalize the sample in the allowed range - return result.map { it.toFloat() / dataRange.last.toFloat() }.toPersistentList() - } -} private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -94,7 +65,7 @@ fun WaveformPlaybackView( linePadding: Dp = 2.dp, minimumGraphAmplitude: Float = 2F, ) { - var seekProgress = remember { mutableStateOf(null) } + val seekProgress = remember { mutableStateOf(null) } var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) } var canvasSizePx by remember { mutableStateOf(Size(0f, 0f)) } val progress by remember(playbackProgress, seekProgress.value) { @@ -105,7 +76,7 @@ fun WaveformPlaybackView( val progressAnimated = animateFloatAsState(targetValue = progress, label = "progressAnimation") val amplitudeDisplayCount by remember(canvasSize) { derivedStateOf { - ((canvasSize.width.value) / (lineWidth.value + linePadding.value)).toInt() + (canvasSize.width.value / (lineWidth.value + linePadding.value)).toInt() } } val normalizedWaveformData by remember(amplitudeDisplayCount) { @@ -124,13 +95,13 @@ fun WaveformPlaybackView( MotionEvent.ACTION_DOWN -> { if (it.x in 0F..canvasSizePx.width) { requestDisallowInterceptTouchEvent.invoke(true) - seekProgress.value = (it.x / canvasSizePx.width) + seekProgress.value = it.x / canvasSizePx.width true } else false } MotionEvent.ACTION_MOVE -> { if (it.x in 0F..canvasSizePx.width) { - seekProgress.value = (it.x / canvasSizePx.width) + seekProgress.value = it.x / canvasSizePx.width } true } @@ -155,7 +126,7 @@ fun WaveformPlaybackView( brush = brush, topLeft = Offset( x = index * (linePadding + lineWidth).toPx(), - y = centerY - (drawingAmplitude / 2) + y = centerY - drawingAmplitude / 2 ), size = Size( width = lineWidth.toPx(), @@ -168,7 +139,7 @@ fun WaveformPlaybackView( drawRect( brush = progressBrush, size = Size( - width = (progressAnimated.value) * canvasSize.width.toPx(), + width = progressAnimated.value * canvasSize.width.toPx(), height = canvasSize.height.toPx() ), blendMode = BlendMode.SrcAtop @@ -178,7 +149,7 @@ fun WaveformPlaybackView( brush = cursorBrush, topLeft = Offset( x = progressAnimated.value * canvasSize.width.toPx(), - y = centerY - ((canvasSize.height.toPx() - 2) / 2) + y = centerY - (canvasSize.height.toPx() - 2) / 2 ), size = Size( width = lineWidth.toPx(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformProgressIndicator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformProgressIndicator.kt deleted file mode 100644 index 94731ef0c8..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformProgressIndicator.kt +++ /dev/null @@ -1,96 +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.voicemessages.timeline - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.unit.dp -import com.linc.audiowaveform.AudioWaveform -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.theme.ElementTheme -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList - -@Composable -fun WaveformProgressIndicator( - progress: Float, - amplitudes: ImmutableList, - modifier: Modifier = Modifier, - onSeek: (progress: Float) -> Unit = {}, -) { - var seekProgress: Float? by remember { mutableStateOf(null) } - val scaledAmplitudes = remember(amplitudes) { amplitudes.scaleAmplitudes() } - AudioWaveform( - modifier = modifier, - waveformBrush = SolidColor(ElementTheme.colors.iconQuaternary), - progressBrush = SolidColor(ElementTheme.colors.iconSecondary), - onProgressChangeFinished = { - // This is to send just one onSeek callback after the user has finished seeking. - // Otherwise the AudioWaveform library would send multiple callbacks while the user is seeking. - val p = seekProgress!! - seekProgress = null - onSeek(p) - }, - spikeWidth = 1.6.dp, - spikeRadius = 0.8.dp, - spikePadding = 3.dp, - progress = seekProgress ?: progress, - amplitudes = scaledAmplitudes, - onProgressChange = { seekProgress = it }, - ) -} - -@PreviewsDayNight -@Composable -internal fun WaveformProgressIndicatorPreview() = ElementPreview { - Column { - WaveformProgressIndicator( - progress = 0.5f, - amplitudes = persistentListOf(), - ) - WaveformProgressIndicator( - progress = 0.5f, - amplitudes = persistentListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0), - ) - WaveformProgressIndicator( - progress = 0.5f, - amplitudes = List(1024) { it }.toPersistentList() - ) - } -} - -/** - * Scale amplitudes to fit in the waveform view. - * - * It seems amplitudes > 128 are clipped by the waveform library. - * Workaround for https://github.com/lincollincol/compose-audiowaveform/issues/22 - * - * TODO Voice messages: Remove this workaround when the waveform library is fixed. - */ -private fun ImmutableList.scaleAmplitudes(): List { - val maxAmplitude = if (isEmpty()) 1 else maxOf { it } - val scalingFactor = 128 / maxAmplitude.toFloat() - return map { (it * scalingFactor).toInt() } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9499d81ee..831fae272e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -166,7 +166,6 @@ maplibre = "org.maplibre.gl:android-sdk:10.2.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.1" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" opusencoder = "io.element.android:opusencoder:1.1.0" -audiowaveform = "com.github.lincollincol:compose-audiowaveform:1.1.1" # Analytics posthog = "com.posthog.android:posthog:2.0.3" diff --git a/settings.gradle.kts b/settings.gradle.kts index 3da4e5efe9..9ac2c96dde 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,7 +35,6 @@ dependencyResolutionManagement { content { includeModule("com.github.UnifiedPush", "android-connector") includeModule("com.github.matrix-org", "matrix-analytics-events") - includeModule("com.github.lincollincol", "compose-audiowaveform") } } // To have immediate access to Rust SDK versions From 5529cb4457df32dc8bbdde019fe208da1963b6ab Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 26 Oct 2023 09:02:31 +0100 Subject: [PATCH 174/281] Remove WaveformProgressIndicator call. --- .../timeline/components/event/TimelineItemVoiceView.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index a1fe5c6526..b4c796c61b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -94,14 +94,6 @@ fun TimelineItemVoiceView( .weight(1f), onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) }, ) -// WaveformProgressIndicator( -// modifier = Modifier -// .height(34.dp) -// .weight(1f), -// progress = state.progress, -// amplitudes = content.waveform, -// onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) } -// ) Spacer(Modifier.width(extraPadding.getDpSize())) } } From 20cef083675a87e1a3e082d0faa0480b29845c8c Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 26 Oct 2023 09:06:08 +0100 Subject: [PATCH 175/281] Don't need added state from bad merge. --- .../voicemessages/timeline/VoiceMessageStateProvider.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt index 823d6dc250..e09c0a0449 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt @@ -35,12 +35,6 @@ open class VoiceMessageStateProvider : PreviewParameterProvider Date: Thu, 26 Oct 2023 09:10:38 +0100 Subject: [PATCH 176/281] -Fix waveform preview --- .../impl/voicemessages/timeline/WaveformPlaybackView.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt index 4759a58ef9..0924932271 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt @@ -21,6 +21,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -165,18 +166,21 @@ fun WaveformPlaybackView( @PreviewsDayNight @Composable internal fun WaveformPlaybackViewPreview() = ElementPreview { - Column { + Column{ WaveformPlaybackView( + modifier = Modifier.height(34.dp), showCursor = false, playbackProgress = 0.5f, waveform = Waveform(persistentListOf()), ) WaveformPlaybackView( + modifier = Modifier.height(34.dp), showCursor = false, playbackProgress = 0.5f, waveform = Waveform(persistentListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)), ) WaveformPlaybackView( + modifier = Modifier.height(34.dp), showCursor = true, playbackProgress = 0.5f, waveform = Waveform(List(1024) { it }.toPersistentList()), From 762e091b06dc96aa690940a1b0461dc2d8a5bba2 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 26 Oct 2023 08:26:46 +0000 Subject: [PATCH 177/281] Update screenshots --- ...l_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png | 4 ++-- ...null_WaveformPlaybackView-D-50_50_null,NEXUS_5,1.0,en].png | 3 +++ ...null_WaveformPlaybackView-N-50_51_null,NEXUS_5,1.0,en].png | 3 +++ ...WaveformProgressIndicator-D-50_50_null,NEXUS_5,1.0,en].png | 3 --- ...WaveformProgressIndicator-N-50_51_null,NEXUS_5,1.0,en].png | 3 --- 36 files changed, 70 insertions(+), 70 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-D-50_50_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-N-50_51_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-D-50_50_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-N-50_51_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png index 3b6f532ca9..1e6264eeac 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357 -size 6094 +oid sha256:8f19f38428d2e17f2cb7a8b3de1af39cf01e48e5c854d3a64208a78c4687b867 +size 6101 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png index acdef50e30..4a9d88076e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd -size 6044 +oid sha256:6a39650b1963d637b42c36d86527124aea32d32de201ebbb6376cf351dcc7182 +size 6076 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png index 3b6f532ca9..2e123fb511 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357 -size 6094 +oid sha256:ef25bb048d7feb642509f1d5bbc646b867b89264efa32e77730650f31f0233ad +size 9895 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png index acdef50e30..eb9c6f5b59 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd -size 6044 +oid sha256:bc14498cb4178dbf6a103159eabe6e0c7f4458f2e10350eacd58a9c0beefff10 +size 10056 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png index e95a96fe33..95680dea95 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e -size 5562 +oid sha256:10966cc6564e45c0d65cdfee2564b7367defc1f673bf62c02291d427655d557c +size 9601 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png index bf834510b6..9bef5959d3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8 -size 5858 +oid sha256:0be1de85e0038df89677884557f9d8a35af8acba29749879469e01ddf095aca7 +size 9933 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png index 4beb9e49a5..54d1cfa607 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1 -size 6035 +oid sha256:dcb3c53fff0cf52bf4ff8f8ca0bafaa7f21d1c7a6d504070de3de8f42dac2864 +size 9906 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png index e95a96fe33..ee35d328f3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e -size 5562 +oid sha256:326b07336560db487616f189e3e39dc5605cabd06e15f836fd153c503ab6fa9d +size 5577 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png index bf834510b6..0c4317b30f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8 -size 5858 +oid sha256:6c70f961cc6ca573216019b3f0fabc18ae05d8bab9bb5719abca1021b9d2350b +size 6089 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png index 4beb9e49a5..b42ebe8d13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1 -size 6035 +oid sha256:0f52f22ad35d11d5124cfcc5128f97e121021dd09caa19c96f23ae8f39aa8cfe +size 6053 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png index 3b6f532ca9..e2de1a2da9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b604356db15171075420241560d26beffd034946545ab4b63b5a03337e61357 -size 6094 +oid sha256:18378d89af53ac54931d3feb3129ff90e0e45c707144322de3d8740fd3ce3645 +size 6436 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png index acdef50e30..663dc09151 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d29d6c39f2bcd7f83d93a0bfac4155089b0b9c24e9de25e4eb1719688c57afd -size 6044 +oid sha256:d95ae8cc103ad50397c804ba2fe16c3b4fb380456f2b3be6f99ec62877b36758 +size 6429 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png index e95a96fe33..e6c7409c2e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0182dbf6c7e2e3e0f9b6e8ccbd232f0d4dc8228ced6c068ce34ff937a6b0241e -size 5562 +oid sha256:10058823be0d5f0a23e1712d4523188729dd7579005a48a63e8e553baea105c0 +size 5936 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png index bf834510b6..ad49bcfa8d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b65132e5ca001b6a721e367dfe6aa40ff162658154522bf5fc29a046c841bee8 -size 5858 +oid sha256:12eacc1babdc55ec2faed630889157ea0c9e6dca8845ecc93bc72bd645f23a9e +size 6481 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png index 4beb9e49a5..80ae6763ba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97d66b9eaf2dce52ce65ba76955ba1d66d7d2c95931b0e0f2ab5c6ae3d1009c1 -size 6035 +oid sha256:2e4ee91fc8599e4c3e1d093587a9688c36df971a94bb832e6662c86572fb08c7 +size 6436 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png index 4f8ac92cfb..223f192095 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031 -size 6027 +oid sha256:1bfffebea253932b08a308ac098a392910233e4a8524a898b459842849854c91 +size 6036 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png index 9a7c22b602..b6cab390c3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290 -size 5992 +oid sha256:b846d71d9d0019f55ee34e4ef38375f1501663a855f2c05742239a08d5b7501d +size 6020 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png index 4f8ac92cfb..1da72edd97 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031 -size 6027 +oid sha256:ca13f10ca5fd6194829c3cef02fa5cd0c00790f23c82d2cf70ac5d1d58babb54 +size 9656 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png index 9a7c22b602..424216fd0a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290 -size 5992 +oid sha256:1dfdaca0c643c81a281658b4cee4d0e938d542a57964c3831496d04db543da45 +size 9827 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png index 5456cf91fa..aec364a0e6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2 -size 5549 +oid sha256:ad2ccde01384563471412843a48180b7acb3ba9bbedb9af60f96d403e564073d +size 9566 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png index 41941771f0..cb45fe4553 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426 -size 5807 +oid sha256:e32db0169cf50c2120a16efea3cb2b0f8a72f81ddce7a303c13bc2bf38c487ff +size 9653 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png index adebad1da6..5288039037 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d -size 5957 +oid sha256:796854c3af1c45268c810f393842287d5fb3424f267f987f47bcd105e2a88448 +size 9621 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png index 5456cf91fa..df1c6e821a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2 -size 5549 +oid sha256:a0a6b88a5d7f001309dcb03da6151ac2949705b32a19301660a24fe02dc6fcdd +size 5562 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png index 41941771f0..9a70f58bb9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426 -size 5807 +oid sha256:2f8562e1e599e16ecc05ec0471de70b9de9a88b347f622bc723f21cb5379b102 +size 6033 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png index adebad1da6..64a01a4889 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d -size 5957 +oid sha256:32eba449e245eed3bc67c1ffdf47e267a6de2bf6832739f2902949af297391aa +size 6001 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png index 4f8ac92cfb..1078abcc5a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00559f4e98cc980c13d3cc5ef867dec8ae5a29b543f18ae7d524a29f3af93031 -size 6027 +oid sha256:a2649d3974b096f17a6e80d728b08409c0f1b28ed649f33c7798f6223f55ca29 +size 6343 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png index 9a7c22b602..b2a3aef0c5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ba19c099d78012318f9f4e7085e4f194be1fd19a0864b516a6b155502ce6290 -size 5992 +oid sha256:7841691e0bbcfa0655304ac6a7c0ea24353aae9bfc264dc7978c2a4526781ee3 +size 6357 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png index 5456cf91fa..a0e382a78c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae9f8e5cc6962138c4ec780beff56bd7643517db192e6860f9ef4fa653c0d5a2 -size 5549 +oid sha256:d1a1ad6a3c02f24a8b811197f6999d93db57d3c1e9b867638c88f4759e9f1b87 +size 5926 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png index 41941771f0..5ec0a0c07f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4b28b61b2550bcd64bada0651fcca78bf19db6af97690d4552831232637b426 -size 5807 +oid sha256:0adbfdd9131913c11d7e4777b4bdfbda84b3a9c6f1784781698d9208023e9c39 +size 6400 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png index adebad1da6..510775c570 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:443284b382e647b380c58c71b35be4fd7194a327af1595ab6af76f648bea281d -size 5957 +oid sha256:cf18143a576ef0a136ca6e07c51521f5d41b6b5f381d168ff311cdf099752550 +size 6349 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png index b218d9da8e..fad12e2edc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f085bc69b2ac416194526e5e6e1f73eb9b0f83acb3b775c80d3a9f88fe6e2eeb -size 22953 +oid sha256:216e36042ad1ef11331d8610ab32383112bde38e314ae0d8c42d23931c7874a2 +size 43748 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png index 1ceb5f0639..8c7ece47b1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acfc96b5c860be6173b38443c7e59c7312c2e6625626855c0efb8552b4bd92f2 -size 22263 +oid sha256:2542c95c067d4d65e76ea606801dc3dbfc196d8128a8547ed3e1bd93b84d4ab9 +size 42513 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-D-50_50_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-D-50_50_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fb4d22eff4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-D-50_50_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efc60fcfb39718bfce62f16e82a0c85249b71bf850b2d996bb1234a882261bc9 +size 10021 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-N-50_51_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-N-50_51_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dfc884aa45 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-N-50_51_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f5b1b3ce84e262eec2f5fea04e4d8d15d0ec4dabd589b73b8b0c0b41afb514 +size 9754 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-D-50_50_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-D-50_50_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 665c8811ac..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-D-50_50_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b -size 4457 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-N-50_51_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-N-50_51_null,NEXUS_5,1.0,en].png deleted file mode 100644 index fae8a6fca3..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformProgressIndicator-N-50_51_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 -size 4464 From c96dc310c8d05f379a4fad77a0e5617ce3675d60 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 26 Oct 2023 09:32:15 +0000 Subject: [PATCH 178/281] Update screenshots --- ...ents_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ents_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png | 4 ++-- ...ll_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ...l.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 3 --- ....setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ....setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png} | 0 ....setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png} | 0 ....setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 3 --- ....setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png | 3 +++ ....setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png} | 0 ....setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png} | 0 ....setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png} | 0 ...lock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png} | 0 ...lock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png} | 0 ...unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png | 3 --- ...nlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png} | 0 ...unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png | 3 +++ ...unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png | 3 +++ ...unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png | 3 --- ...nlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png} | 0 ...unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png | 3 +++ ...unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png | 3 +++ ...ncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png | 4 ++-- ...ncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png | 4 ++-- ...ll_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...l_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...l_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- 50 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png index b2f7ce4747..28583d7fa6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de1d44f62edd3f2d30421e49ad435971b165711b8bfe6d4a9475c5fb2f9f83ed -size 8506 +oid sha256:dcd61b2ec311170adfde3dc4a5e73ab216d2c9030e0b5893ff7c518488d01bb2 +size 8103 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png index a4c7739624..4899424130 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d58d2d25d8f2c07c976bcda0eea7ec101f993ad1ef733fae4c713a67650e33e2 -size 8498 +oid sha256:a5b714f3dec62d10711e977e88d536b8bd78c60eabd1a718e2f70a160e6582c2 +size 8109 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..978b895828 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55699e4df8454188d18cff9476f52ad73066b740c67b15eafaf4003cb3bee62f +size 18567 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..678036c4f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e36041959b02f320438ae5627559b52538a79e5bf813c5beec2dc30c0ba9e61 +size 20909 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bc57c63a27 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6ca257ae106de5d4361464132d97febacc09f211e5697d0da03c767fd8a2d05 +size 32036 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dfb8d18f18 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71b5f1d9d01cd1c4ba0fc34c636fefb54099540586097d2c789befb36f69e68e +size 17124 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4fc8cff00a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2eb7e4911680f6660823a685e1fa8bf12ed623955685792dcef80ac7b25fb26 +size 19438 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2bdb7383d8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9968ea696d5f88103b8ae6c03c58e87799dd9f22d831614d0563b343c49e931b +size 28582 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 0c72d54e99..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d25ab8d3f53a5139b265a1ddef43ef3755b539a695aa8f43615e9197bd93471d -size 33231 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a27cd2534b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae1ac3fa371e4deb0851baa75a5e68635d8ee45eaea6dd8e373768df8cf237a1 +size 32925 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index b22bfb46dd..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d5037348c8b27714e54ebec53aac8a546f14b11e3195434f681aad95e416847 -size 32181 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-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.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8508c0ba62 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:881e580f855b653e5b4159f18c948aaf5636bebca97d0f687c3bdb32bd805a46 +size 31899 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-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.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index ee9f61a453..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:28d2e1634cc0bbf01f6efc2b260625abb26f2105bf1e858f2bd196830d70854c -size 44278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3cc563d5a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0d360c0697733a9e1067589be5178f631da673743b97daeb50ebdc51fd89502 +size 44289 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3cc91d32f0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc653f37c55ce7f9ef001d8f8ab57de23008baae2f644c6db49a7f4bccc831d5 +size 34938 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index bb647daed5..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d50c34cdf50881d8176d90617c717f7214faeaa4fdce22ec4b93b0e5669b9869 -size 39073 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ac4cc89932 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5b8afd4e3adca2e42463b66b5da449caaed61ee8ba9240a8a5e93f2c7ce75b4 +size 39061 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dc79b17cbd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd06f078cff08947537a58ddc2df9a79f4aa3d55372b6b8eafa962f2266ca02f +size 31443 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png index 57ff1f481f..aabcc28af3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc16dd2b27cf281c39932013a904a72bb6b417a07105d3d51b96f4b49a642e25 -size 14865 +oid sha256:38ae3e66f693072c63a13df6b016d9c29d8e15eb966d0dd43d0443a4f9e37839 +size 13396 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png index a565274d3c..9a73fd5309 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b93e0fc28c37a90991ff9778f40fc8e9b07da593d21c6bfc73451006ef6d067c -size 14415 +oid sha256:ce47e90d5cc8d336bc965ae879ab69fc1482254fbd3be524df8eb3c7b40b98df +size 13026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png index 9e6006a88a..79ac6aa584 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81ab934cdcd8e7ad76c9bce474b59fbe449960ac2abf2d7b4fa85df89270fc25 -size 46489 +oid sha256:eec57772a4c2390238b7363d91d02cc99124ca442a66a9049155f86313506b43 +size 45307 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png index 11092fbadf..a142e1a34b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1a66ecf4b07ec0e57d1d5e92f9af7e4989288dec756eedef2b93d8b6b980424 -size 45815 +oid sha256:06eaf42a48e6ece74f4192de66731e4dfc23617034ddf1cbedb18eb392e3e10a +size 44638 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png index c0b3ad3ba5..5b33dfe72e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49cf3240371d9311320681520de8f44f5f8a013ba61949023a25e43b2960f513 -size 49806 +oid sha256:f860899fb9c33ab3af97dee35bcd39d9b4c128fed4c0256b2c354dc9193d1a0b +size 48393 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png index d9e8c5bdea..80cb7c6241 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12afe63bad3f3586d10c4e59e9cc05ec038cc47942ebead5bed11e58b119e648 -size 49686 +oid sha256:a3eb0577c673b6da2bbf35d4eeb485c2af08cdea3e084dbf123befb2d092bbcd +size 48300 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png index 502407ff36..a578d69128 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31363274a1a4205e24640d1b02d32108afcabc62ee019da8f85442562fcf1b32 -size 32713 +oid sha256:ac49f8798ffa60757fcda059c52dffe9f16271bf507f917dffadd20e461acd2a +size 31866 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png index 1a95753f0a..cffb3cf5d8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ba708a9862d0b67b444ec1f92e076e8d6b0be27d3d519b997c2270add4c5ace -size 31946 +oid sha256:aa94619e6f4022ee84aca4e31393de336545b7cf6d73c0a025a1cca68ce713e0 +size 31099 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png index 58dae239ef..19e14046b1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c31acff0050557fee63af7653416990e72d0b865d1f7dc8c572f197a330f891 -size 32491 +oid sha256:d0c1dff252324c71845437bd7fd261e7faacf98030365f3cca29763a1bf8ce24 +size 31645 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png index 207e50c689..956d853953 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abddf374b03a92fc98014c693cad5aca21cab5d79e781016ac761dc1e1242ec2 -size 25233 +oid sha256:fcacc94648c996c477eb4b0179df4eeffab6d1f7106cc656c548048ae33d4476 +size 24572 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png index c59c20845e..c91ef3380a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4557db65e49c39dcec5a74d44d1662bef08a7c557027f8cf0af494e8262edb9 -size 28389 +oid sha256:a7a12d96b619106be6075b39343582044062affaa58c3fe46cd04c570b08c34d +size 27650 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png index 60236462b3..54dbd92c83 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e4d70183cbb0d145850007c894bda6bec14e1ab955cecc362c4842de4455e8b -size 27548 +oid sha256:d3e6a1cf98bca50a1949cdaf27663cfbe5d2bb6f829a106f6a4291cb4b643ce1 +size 26805 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png index 8c22c300dc..27a3608686 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf1535f9bfe230c0fd8cd777ac3afefed5d1dd383a5a17eceffd466e6712477b -size 28149 +oid sha256:a7c42827f60bbe46846284cf135c15402abef93a404fc8ffa85cb27fb386d744 +size 27421 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png index 0793b6e369..0a1d99b585 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70677ed0d1f162a17414deb276cfe7b20ea1b8a76f688b6566c67c2fe984a2ee -size 21679 +oid sha256:3775126d27af04166dac57b67e14d4f0bd0ac2baf76a00bc665bbc8e30c7c716 +size 21025 From 7d1b6714268acc6173a81b64d7481166c62b7564 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 26 Oct 2023 12:51:12 +0200 Subject: [PATCH 179/281] Refactor of voice message playing code (#1643) After PR review suggestions from @jonnyandrew User facing functionality doesn't change, but overall architecture and testing is better. --- .../timeline/VoiceMessageCache.kt | 123 ------------ .../timeline/VoiceMessageMediaRepo.kt | 137 +++++++++++++ .../timeline/VoiceMessagePlayer.kt | 108 ++++++----- .../timeline/VoiceMessagePresenter.kt | 48 +---- .../DefaultVoiceMessageMediaRepoTest.kt | 142 ++++++++++++++ .../timeline/DefaultVoiceMessagePlayerTest.kt | 149 ++++++++++++++ ...eCache.kt => FakeVoiceMessageMediaRepo.kt} | 34 ++-- .../timeline/VoiceMessageCacheTest.kt | 90 --------- .../timeline/VoiceMessagePresenterTest.kt | 182 +++++------------- .../matrix/test/media/FakeMediaLoader.kt | 3 +- 10 files changed, 562 insertions(+), 454 deletions(-) delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageCache.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt rename features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/{FakeVoiceMessageCache.kt => FakeVoiceMessageMediaRepo.kt} (52%) delete mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessageCacheTest.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageCache.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageCache.kt deleted file mode 100644 index 8ced58132e..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageCache.kt +++ /dev/null @@ -1,123 +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.voicemessages.timeline - -import com.squareup.anvil.annotations.ContributesBinding -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.CacheDirectory -import java.io.File - -/** - * Manages the local disk cache for a voice message. - */ -interface VoiceMessageCache { - - /** - * Factory for [VoiceMessageCache]. - */ - fun interface Factory { - /** - * Creates a [VoiceMessageCache] for the given Matrix Content (mxc://) URI. - * - * @param mxcUri the Matrix Content (mxc://) URI of the voice message. - */ - fun create(mxcUri: String): VoiceMessageCache - } - - /** - * The file path of the voice message in the cache directory. - * NB: This doesn't necessarily mean that the file exists. - * - * @return the file path of the voice message in the cache directory. - */ - val cachePath: String - - /** - * Checks if the voice message is in the cache directory. - * - * @return true if the voice message is in the cache directory. - */ - fun isInCache(): Boolean - - /** - * Moves the file to the voice cache directory. - * - * @return true if the file was successfully moved. - */ - fun moveToCache(file: File): Boolean -} - -/** - * Default implementation of [VoiceMessageCache]. - * - * NB: All methods will throw an [IllegalStateException] if the mxcUri is invalid. - * - * @param cacheDir the application's cache directory. - * @param mxcUri the Matrix Content (mxc://) URI of the voice message. - */ -class VoiceMessageCacheImpl @AssistedInject constructor( - @CacheDirectory private val cacheDir: File, - @Assisted private val mxcUri: String, -) : VoiceMessageCache { - - @ContributesBinding(AppScope::class) - @AssistedFactory - fun interface Factory : VoiceMessageCache.Factory { - override fun create(mxcUri: String): VoiceMessageCacheImpl - } - - override val cachePath: String = "${cacheDir.path}/$CACHE_VOICE_SUBDIR/${mxcUri2FilePath(mxcUri)}" - - override fun isInCache(): Boolean = File(cachePath).exists() - - override fun moveToCache(file: File): Boolean { - val dest = File(cachePath).apply { parentFile?.mkdirs() } - return file.renameTo(dest) - } -} - -/** - * Subdirectory of the application's cache directory where voice messages are stored. - */ -private const val CACHE_VOICE_SUBDIR = "temp/voice" - -/** - * Regex to match a Matrix Content (mxc://) URI. - * - * See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris - */ -private val mxcRegex = Regex("""^mxc:\/\/([^\/]+)\/([^\/]+)$""") - -/** - * Sanitizes an mxcUri to be used as a relative file path. - * - * @param mxcUri the Matrix Content (mxc://) URI of the voice message. - * @return the relative file path as "/". - * @throws IllegalStateException if the mxcUri is invalid. - */ -private fun mxcUri2FilePath(mxcUri: String): String = checkNotNull(mxcRegex.matchEntire(mxcUri)) { - "mxcUri2FilePath: Invalid mxcUri: $mxcUri" -}.let { match -> - buildString { - append(match.groupValues[1]) - append("/") - append(match.groupValues[2]) - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt new file mode 100644 index 0000000000..a559183261 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt @@ -0,0 +1,137 @@ +/* + * 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.voicemessages.timeline + +import com.squareup.anvil.annotations.ContributesBinding +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.media.toFile +import java.io.File + +/** + * Fetches the media file for a voice message. + * + * Media is downloaded from the rust sdk and stored in the application's cache directory. + * Media files are indexed by their Matrix Content (mxc://) URI and considered immutable. + * Whenever a given mxc is found in the cache, it is returned immediately. + */ +interface VoiceMessageMediaRepo { + + /** + * Factory for [VoiceMessageMediaRepo]. + */ + fun interface Factory { + /** + * Creates a [VoiceMessageMediaRepo]. + * + * @param mediaSource the media source of the voice message. + * @param mimeType the mime type of the voice message. + * @param body the body of the voice message. + */ + fun create( + mediaSource: MediaSource, + mimeType: String?, + body: String?, + ): VoiceMessageMediaRepo + } + + /** + * Returns the voice message media file. + * + * In case of a cache hit the file is returned immediately. + * In case of a cache miss the file is downloaded and then returned. + * + * @return A [Result] holding either the media [File] from the cache directory or an [Exception]. + */ + suspend fun getMediaFile(): Result +} + +class DefaultVoiceMessageMediaRepo @AssistedInject constructor( + @CacheDirectory private val cacheDir: File, + private val matrixMediaLoader: MatrixMediaLoader, + @Assisted private val mediaSource: MediaSource, + @Assisted("mimeType") private val mimeType: String?, + @Assisted("body") private val body: String?, +) : VoiceMessageMediaRepo { + + @ContributesBinding(RoomScope::class) + @AssistedFactory + fun interface Factory : VoiceMessageMediaRepo.Factory { + override fun create( + mediaSource: MediaSource, + @Assisted("mimeType") mimeType: String?, + @Assisted("body") body: String?, + ): DefaultVoiceMessageMediaRepo + } + + override suspend fun getMediaFile(): Result = if (!isInCache()) { + matrixMediaLoader.downloadMediaFile( + source = mediaSource, + mimeType = mimeType, + body = body, + ).mapCatching { + val dest = cachedFilePath.apply { parentFile?.mkdirs() } + // TODO By not closing the MediaFile we're leaking the rust file handle here. + // Not that big of a deal but better to avoid it someday. + if (it.toFile().renameTo(dest)) { + dest + } else { + error("Failed to move file to cache.") + } + } + } else { + Result.success(cachedFilePath) + } + + private val cachedFilePath: File = File("${cacheDir.path}/$CACHE_VOICE_SUBDIR/${mxcUri2FilePath(mediaSource.url)}") + + private fun isInCache(): Boolean = cachedFilePath.exists() +} + +/** + * Subdirectory of the application's cache directory where voice messages are stored. + */ +private const val CACHE_VOICE_SUBDIR = "temp/voice" + +/** + * Regex to match a Matrix Content (mxc://) URI. + * + * See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris + */ +private val mxcRegex = Regex("""^mxc:\/\/([^\/]+)\/([^\/]+)$""") + +/** + * Sanitizes an mxcUri to be used as a relative file path. + * + * @param mxcUri the Matrix Content (mxc://) URI of the voice message. + * @return the relative file path as "/". + * @throws IllegalStateException if the mxcUri is invalid. + */ +private fun mxcUri2FilePath(mxcUri: String): String = checkNotNull(mxcRegex.matchEntire(mxcUri)) { + "mxcUri2FilePath: Invalid mxcUri: $mxcUri" +}.let { match -> + buildString { + append(match.groupValues[1]) + append("/") + append(match.groupValues[2]) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt index a2d4f0b655..3934c88b9c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt @@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.messages.impl.mediaplayer.MediaPlayer import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.media.MediaSource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -37,14 +38,20 @@ interface VoiceMessagePlayer { * * NB: Different voice messages can use the same content uri (e.g. in case of * a forward of a voice message), - * therefore the media uri is not enough to uniquely identify a voice message. - * This is why we must provide the eventId as well. + * therefore the mxc:// uri in [mediaSource] is not enough to uniquely identify + * a voice message. This is why we must provide the eventId as well. * - * @param eventId The id of the voice message event. If null, a dummy - * player is returned. - * @param mediaPath The path to the voice message's media file. + * @param eventId The eventId of the voice message event. + * @param mediaSource The media source of the voice message. + * @param mimeType The mime type of the voice message. + * @param body The body of the voice message. */ - fun create(eventId: EventId?, mediaPath: String): VoiceMessagePlayer + fun create( + eventId: EventId?, + mediaSource: MediaSource, + mimeType: String?, + body: String?, + ): VoiceMessagePlayer } /** @@ -53,15 +60,14 @@ interface VoiceMessagePlayer { val state: Flow /** - * Start playing from the beginning acquiring control of the - * underlying [MediaPlayer]. + * Starts playing from the beginning + * acquiring control of the underlying [MediaPlayer]. + * If already in control of the underlying [MediaPlayer], starts playing from the + * current position. + * + * Will suspend whilst the media file is being downloaded. */ - fun acquireControlAndPlay() - - /** - * Start playing from the current position. - */ - fun play() + suspend fun play(): Result /** * Pause playback. @@ -92,32 +98,45 @@ interface VoiceMessagePlayer { } /** - * An implementation of [VoiceMessagePlayer] which is backed by a [MediaPlayer] - * usually shared among different [VoiceMessagePlayer] instances. - * - * @param mediaPlayer The [MediaPlayer] to use. - * @param eventId The id of the voice message event. If null, the player will behave as no-op. - * @param mediaPath The path to the voice message's media file. + * An implementation of [VoiceMessagePlayer] which is backed by a + * [VoiceMessageMediaRepo] to fetch and cache the media file and + * which uses a global [MediaPlayer] instance to play the media. */ -class VoiceMessagePlayerImpl( +class DefaultVoiceMessagePlayer( private val mediaPlayer: MediaPlayer, + voiceMessageMediaRepoFactory: VoiceMessageMediaRepo.Factory, private val eventId: EventId?, - private val mediaPath: String, + mediaSource: MediaSource, + mimeType: String?, + body: String?, ) : VoiceMessagePlayer { @ContributesBinding(RoomScope::class) // Scoped types can't use @AssistedInject. class Factory @Inject constructor( private val mediaPlayer: MediaPlayer, + private val voiceMessageMediaRepoFactory: VoiceMessageMediaRepo.Factory, ) : VoiceMessagePlayer.Factory { - override fun create(eventId: EventId?, mediaPath: String): VoiceMessagePlayerImpl { - return VoiceMessagePlayerImpl( - mediaPlayer = mediaPlayer, - eventId = eventId, - mediaPath = mediaPath, - ) - } + override fun create( + eventId: EventId?, + mediaSource: MediaSource, + mimeType: String?, + body: String?, + ): DefaultVoiceMessagePlayer = DefaultVoiceMessagePlayer( + mediaPlayer = mediaPlayer, + voiceMessageMediaRepoFactory = voiceMessageMediaRepoFactory, + eventId = eventId, + mediaSource = mediaSource, + mimeType = mimeType, + body = body, + ) } + private val repo = voiceMessageMediaRepoFactory.create( + mediaSource = mediaSource, + mimeType = mimeType, + body = body + ) + override val state: Flow = mediaPlayer.state.map { state -> VoiceMessagePlayer.State( isPlaying = state.mediaId.isMyTrack() && state.isPlaying, @@ -126,19 +145,20 @@ class VoiceMessagePlayerImpl( ) }.distinctUntilChanged() - override fun acquireControlAndPlay() { - eventId?.let { eventId -> - mediaPlayer.acquireControlAndPlay( - uri = mediaPath, - mediaId = eventId.value, - mimeType = "audio/ogg" // Files in the voice cache have no extension so we need to set the mime type manually. - ) - } - } - - override fun play() { - ifInControl { - mediaPlayer.play() + override suspend fun play(): Result = if (inControl()) { + mediaPlayer.play() + Result.success(Unit) + } else { + if (eventId != null) { + repo.getMediaFile().mapCatching { mediaFile -> + mediaPlayer.acquireControlAndPlay( + uri = mediaFile.path, + mediaId = eventId.value, + mimeType = "audio/ogg" // Files in the voice cache have no extension so we need to set the mime type manually. + ) + } + } else { + Result.failure(IllegalStateException("Cannot play a voice message with no eventId")) } } @@ -157,6 +177,8 @@ class VoiceMessagePlayerImpl( private fun String?.isMyTrack(): Boolean = if (eventId == null) false else this == eventId.value private inline fun ifInControl(block: () -> Unit) { - if (mediaPlayer.state.value.mediaId.isMyTrack()) block() + if (inControl()) block() } + + private fun inControl(): Boolean = mediaPlayer.state.value.mediaId.isMyTrack() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt index f94176b601..6bff6ef26f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt @@ -37,9 +37,6 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.media.MatrixMediaLoader -import io.element.android.libraries.matrix.api.media.MediaFile -import io.element.android.libraries.matrix.api.media.toFile import io.element.android.libraries.ui.utils.time.formatShort import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.milliseconds @@ -54,9 +51,7 @@ interface VoiceMessagePresenterModule { } class VoiceMessagePresenter @AssistedInject constructor( - private val mediaLoader: MatrixMediaLoader, voiceMessagePlayerFactory: VoiceMessagePlayer.Factory, - voiceMessageCacheFactory: VoiceMessageCache.Factory, @Assisted private val content: TimelineItemVoiceContent, ) : Presenter { @@ -65,11 +60,11 @@ class VoiceMessagePresenter @AssistedInject constructor( override fun create(content: TimelineItemVoiceContent): VoiceMessagePresenter } - private val voiceCache = voiceMessageCacheFactory.create(mxcUri = content.mediaSource.url) - private val player = voiceMessagePlayerFactory.create( eventId = content.eventId, - mediaPath = voiceCache.cachePath + mediaSource = content.mediaSource, + mimeType = content.mimeType, + body = content.body, ) @Composable @@ -78,15 +73,15 @@ class VoiceMessagePresenter @AssistedInject constructor( val scope = rememberCoroutineScope() val playerState by player.state.collectAsState(VoiceMessagePlayer.State(isPlaying = false, isMyMedia = false, currentPosition = 0L)) - val mediaFile = remember { mutableStateOf>(Async.Uninitialized) } + val play = remember { mutableStateOf>(Async.Uninitialized) } val button by remember { derivedStateOf { when { content.eventId == null -> VoiceMessageState.Button.Disabled playerState.isPlaying -> VoiceMessageState.Button.Pause - mediaFile.value is Async.Loading -> VoiceMessageState.Button.Downloading - mediaFile.value is Async.Failure -> VoiceMessageState.Button.Retry + play.value is Async.Loading -> VoiceMessageState.Button.Downloading + play.value is Async.Failure -> VoiceMessageState.Button.Retry else -> VoiceMessageState.Button.Play } } @@ -101,38 +96,13 @@ class VoiceMessagePresenter @AssistedInject constructor( } } - suspend fun downloadCacheAndPlay() { - mediaFile.runUpdatingState { - mediaLoader.downloadMediaFile( - source = content.mediaSource, - mimeType = content.mimeType, - body = content.body, - ).mapCatching { - if (voiceCache.moveToCache(it.toFile())) { - player.acquireControlAndPlay() - it - } else { - error("Failed to move file to cache.") - } - } - } - } - fun eventSink(event: VoiceMessageEvents) { when (event) { is VoiceMessageEvents.PlayPause -> { - if (playerState.isMyMedia) { - if (playerState.isPlaying) { - player.pause() - } else { - player.play() - } + if (playerState.isPlaying) { + player.pause() } else { - if (voiceCache.isInCache()) { - player.acquireControlAndPlay() - } else { - scope.launch { downloadCacheAndPlay() } - } + scope.launch { play.runUpdatingState { player.play() } } } } is VoiceMessageEvents.Seek -> { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt new file mode 100644 index 0000000000..3b19e66450 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt @@ -0,0 +1,142 @@ +/* + * 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.voicemessages.timeline + +import com.google.common.truth.Truth +import io.element.android.features.messages.impl.voicemessages.timeline.DefaultVoiceMessageMediaRepo +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.test.media.FakeMediaLoader +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class DefaultVoiceMessageMediaRepoTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `cache miss - downloads and returns cached file successfully`() = runTest { + val fakeMediaLoader = FakeMediaLoader().apply { + path = temporaryFolder.createRustMediaFile().path + } + val repo = createDefaultVoiceMessageMediaRepo( + temporaryFolder = temporaryFolder, + matrixMediaLoader = fakeMediaLoader, + ) + + repo.getMediaFile().let { result -> + Truth.assertThat(result.isSuccess).isTrue() + result.getOrThrow().let { file -> + Truth.assertThat(file.path).isEqualTo(temporaryFolder.cachedFilePath) + Truth.assertThat(file.exists()).isTrue() + } + } + } + + @Test + fun `cache miss - download fails`() = runTest { + val fakeMediaLoader = FakeMediaLoader().apply { + shouldFail = true + } + val repo = createDefaultVoiceMessageMediaRepo( + temporaryFolder = temporaryFolder, + matrixMediaLoader = fakeMediaLoader, + ) + + repo.getMediaFile().let { result -> + Truth.assertThat(result.isFailure).isTrue() + result.exceptionOrNull()?.let { exception -> + Truth.assertThat(exception).isInstanceOf(RuntimeException::class.java) + } + } + } + + @Test + fun `cache miss - download succeeds but file move fails`() = runTest { + val fakeMediaLoader = FakeMediaLoader().apply { + path = temporaryFolder.createRustMediaFile().path + } + File(temporaryFolder.cachedFilePath).apply { + parentFile?.mkdirs() + // Deny access to parent folder so move to cache will fail. + parentFile?.setReadable(false) + parentFile?.setWritable(false) + parentFile?.setExecutable(false) + } + val repo = createDefaultVoiceMessageMediaRepo( + temporaryFolder = temporaryFolder, + matrixMediaLoader = fakeMediaLoader, + ) + + repo.getMediaFile().let { result -> + Truth.assertThat(result.isFailure).isTrue() + result.exceptionOrNull()?.let { exception -> + Truth.assertThat(exception).apply { + isInstanceOf(IllegalStateException::class.java) + hasMessageThat().isEqualTo("Failed to move file to cache.") + } + } + } + } + + @Test + fun `cache hit - returns cached file successfully`() = runTest { + temporaryFolder.createCachedFile() + val fakeMediaLoader = FakeMediaLoader().apply { + shouldFail = true // so that if we hit the media loader it will crash + } + val repo = createDefaultVoiceMessageMediaRepo( + temporaryFolder = temporaryFolder, + matrixMediaLoader = fakeMediaLoader, + ) + + repo.getMediaFile().let { result -> + Truth.assertThat(result.isSuccess).isTrue() + result.getOrThrow().let { file -> + Truth.assertThat(file.path).isEqualTo(temporaryFolder.cachedFilePath) + Truth.assertThat(file.exists()).isTrue() + } + } + } +} + +private fun createDefaultVoiceMessageMediaRepo( + temporaryFolder: TemporaryFolder, + matrixMediaLoader: MatrixMediaLoader = FakeMediaLoader(), +) = DefaultVoiceMessageMediaRepo( + cacheDir = temporaryFolder.root, + matrixMediaLoader = matrixMediaLoader, + mediaSource = MediaSource( + url = MXC_URI, + json = null + ), + mimeType = "audio/ogg", + body = "someBody.ogg" +) + +private const val MXC_URI = "mxc://matrix.org/1234567890abcdefg" +private val TemporaryFolder.cachedFilePath get() = "${this.root.path}/temp/voice/matrix.org/1234567890abcdefg" +private fun TemporaryFolder.createCachedFile() = File(cachedFilePath).apply { + parentFile?.mkdirs() + createNewFile() +} + +private fun TemporaryFolder.createRustMediaFile() = File(this.root, "rustMediaFile.ogg").apply { createNewFile() } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt new file mode 100644 index 0000000000..397a4a0373 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt @@ -0,0 +1,149 @@ +/* + * 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.voicemessages.timeline + +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.features.messages.impl.mediaplayer.MediaPlayer +import io.element.android.features.messages.impl.voicemessages.timeline.DefaultVoiceMessagePlayer +import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageMediaRepo +import io.element.android.features.messages.mediaplayer.FakeMediaPlayer +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultVoiceMessagePlayerTest { + + @Test + fun `initial state`() = runTest { + createDefaultVoiceMessagePlayer().state.test { + awaitItem().let { + Truth.assertThat(it.isPlaying).isEqualTo(false) + Truth.assertThat(it.isMyMedia).isEqualTo(false) + Truth.assertThat(it.currentPosition).isEqualTo(0) + } + } + } + + @Test + fun `downloading and play works`() = runTest { + val player = createDefaultVoiceMessagePlayer() + player.state.test { + skipItems(1) // skip initial state. + Truth.assertThat(player.play().isSuccess).isTrue() + awaitItem().let { + Truth.assertThat(it.isPlaying).isEqualTo(true) + Truth.assertThat(it.isMyMedia).isEqualTo(true) + Truth.assertThat(it.currentPosition).isEqualTo(1000) + } + } + } + + @Test + fun `downloading and play fails`() = runTest { + val player = createDefaultVoiceMessagePlayer( + voiceMessageMediaRepo = FakeVoiceMessageMediaRepo().apply { + shouldFail = true + }, + ) + player.state.test { + skipItems(1) // skip initial state. + Truth.assertThat(player.play().isFailure).isTrue() + } + } + + @Test + fun `play fails with no eventId`() = runTest { + val player = createDefaultVoiceMessagePlayer( + eventId = null + ) + player.state.test { + skipItems(1) // skip initial state. + Truth.assertThat(player.play().isFailure).isTrue() + } + } + + @Test + fun `pause playing works`() = runTest { + val player = createDefaultVoiceMessagePlayer() + player.state.test { + skipItems(1) // skip initial state. + Truth.assertThat(player.play().isSuccess).isTrue() + skipItems(1) // skip play state + player.pause() + awaitItem().let { + Truth.assertThat(it.isPlaying).isEqualTo(false) + Truth.assertThat(it.isMyMedia).isEqualTo(true) + Truth.assertThat(it.currentPosition).isEqualTo(1000) + } + } + } + + @Test + fun `play after pause works`() = runTest { + val player = createDefaultVoiceMessagePlayer() + player.state.test { + skipItems(1) // skip initial state. + Truth.assertThat(player.play().isSuccess).isTrue() + skipItems(1) // skip play state + player.pause() + skipItems(1) + player.play() + awaitItem().let { + Truth.assertThat(it.isPlaying).isEqualTo(true) + Truth.assertThat(it.isMyMedia).isEqualTo(true) + Truth.assertThat(it.currentPosition).isEqualTo(2000) + } + } + } + + @Test + fun `seek to works`() = runTest { + val player = createDefaultVoiceMessagePlayer() + player.state.test { + skipItems(1) // skip initial state. + Truth.assertThat(player.play().isSuccess).isTrue() + skipItems(1) // skip play state + player.seekTo(2000) + awaitItem().let { + Truth.assertThat(it.isPlaying).isEqualTo(true) + Truth.assertThat(it.isMyMedia).isEqualTo(true) + Truth.assertThat(it.currentPosition).isEqualTo(2000) + } + } + } +} + +private fun createDefaultVoiceMessagePlayer( + mediaPlayer: MediaPlayer = FakeMediaPlayer(), + voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(), + eventId: EventId? = AN_EVENT_ID, +) = DefaultVoiceMessagePlayer( + mediaPlayer = mediaPlayer, + voiceMessageMediaRepoFactory = { _, _, _ -> voiceMessageMediaRepo }, + eventId = eventId, + mediaSource = MediaSource( + url = MXC_URI, + json = null + ), + mimeType = "audio/ogg", + body = "someBody.ogg" +) + +private const val MXC_URI = "mxc://matrix.org/1234567890abcdefg" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeVoiceMessageCache.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeVoiceMessageMediaRepo.kt similarity index 52% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeVoiceMessageCache.kt rename to features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeVoiceMessageMediaRepo.kt index ee61a690e6..198b65e445 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeVoiceMessageCache.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/FakeVoiceMessageMediaRepo.kt @@ -16,34 +16,22 @@ package io.element.android.features.messages.voicemessages.timeline -import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageCache +import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageMediaRepo +import io.element.android.tests.testutils.simulateLongTask import java.io.File /** - * A fake implementation of [VoiceMessageCache] for testing purposes. + * A fake implementation of [VoiceMessageMediaRepo] for testing purposes. */ -class FakeVoiceMessageCache : VoiceMessageCache { +class FakeVoiceMessageMediaRepo : VoiceMessageMediaRepo { - private var _cachePath: String = "" - private var _isInCache: Boolean = false - private var _moveToCache: Boolean = false + var shouldFail = false - override val cachePath: String - get() = _cachePath - - override fun isInCache(): Boolean = _isInCache - - override fun moveToCache(file: File): Boolean = _moveToCache - - fun givenCachePath(cachePath: String) { - _cachePath = cachePath - } - - fun givenIsInCache(isInCache: Boolean) { - _isInCache = isInCache - } - - fun givenMoveToCache(moveToCache: Boolean) { - _moveToCache = moveToCache + override suspend fun getMediaFile(): Result = simulateLongTask { + if (shouldFail) { + Result.failure(IllegalStateException("Failed to get media file")) + } else { + Result.success(File("")) + } } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessageCacheTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessageCacheTest.kt deleted file mode 100644 index 3c7a6468a0..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessageCacheTest.kt +++ /dev/null @@ -1,90 +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.voicemessages.timeline - -import com.google.common.truth.Truth -import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageCacheImpl -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import java.io.File - -class VoiceMessageCacheTest { - - @get:Rule - val temporaryFolder = TemporaryFolder() - - @Test - fun `moveToVoiceCache() should move the file to the voice cache dir`() { - val rootPath = temporaryFolder.root.path - val file = File("$rootPath/myFile.txt").apply { createNewFile() } - val cacheDir = File("$rootPath/cacheDir").apply { if (!exists()) mkdirs() } - val mxcUri = "mxc://matrix.org/1234567890abcdefg" - val cache = VoiceMessageCacheImpl(cacheDir, mxcUri) - - Truth.assertThat(cache.moveToCache(file)) - .isTrue() - Truth.assertThat(File("$rootPath/cacheDir/temp/voice/matrix.org/1234567890abcdefg").exists()) - .isTrue() - } - - @Test - fun `voiceCachePath() should point to cacheDir-temp-voice-mxcUri2fileName`() { - val rootPath = temporaryFolder.root.path - val cacheDir = File("$rootPath/cacheDir") - val mxcUri = "mxc://matrix.org/1234567890abcdefg" - val cache = VoiceMessageCacheImpl(cacheDir, mxcUri) - - Truth.assertThat(cache.cachePath) - .isEqualTo("$rootPath/cacheDir/temp/voice/matrix.org/1234567890abcdefg") - } - - @Test - fun `isInVoiceCache() should return true if the file exists`() { - val rootPath = temporaryFolder.root.path - val cacheDir = File("$rootPath/cacheDir") - val mxcUri = "mxc://matrix.org/1234567890abcdefg" - val file = File("$rootPath/cacheDir/temp/voice/matrix.org/1234567890abcdefg").apply { - parentFile?.mkdirs() - createNewFile() - } - val cache = VoiceMessageCacheImpl(cacheDir, mxcUri) - - Truth.assertThat(cache.isInCache()) - .isTrue() - } - - @Test - fun `isInVoiceCache() should return false if the file does not exist`() { - val rootPath = temporaryFolder.root.path - val cacheDir = File("$rootPath/cacheDir") - val mxcUri = "mxc://matrix.org/1234567890abcdefg" - val cache = VoiceMessageCacheImpl(cacheDir, mxcUri) - - Truth.assertThat(cache.isInCache()) - .isFalse() - } - - @Test(expected = IllegalStateException::class) - fun `isInVoiceCache() throws IllegalStateException on bogus mxc uri`() { - val cacheDir = File("") - val mxcUri = "bogus" - val cache = VoiceMessageCacheImpl(cacheDir, mxcUri) - - cache.isInCache() - } -} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt index c748679421..0a6ca3b8c4 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt @@ -22,23 +22,19 @@ import app.cash.turbine.test import com.google.common.truth.Truth import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent -import io.element.android.features.messages.mediaplayer.FakeMediaPlayer +import io.element.android.features.messages.impl.voicemessages.timeline.DefaultVoiceMessagePlayer import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents -import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessagePlayerImpl +import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageMediaRepo import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessagePresenter import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState -import io.element.android.libraries.matrix.test.media.FakeMediaLoader +import io.element.android.features.messages.mediaplayer.FakeMediaPlayer import kotlinx.coroutines.test.runTest import org.junit.Test class VoiceMessagePresenterTest { - - private val fakeMediaLoader = FakeMediaLoader() - private val fakeVoiceCache = FakeVoiceMessageCache() - @Test fun `initial state has proper default values`() = runTest { - val presenter = createVoiceMessagePresenter(fakeMediaLoader, fakeVoiceCache) + val presenter = createVoiceMessagePresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -51,12 +47,10 @@ class VoiceMessagePresenterTest { } @Test - fun `pressing play with file in cache plays`() = runTest { - fakeVoiceCache.apply { - givenIsInCache(true) - } - val content = aTimelineItemVoiceContent(durationMs = 2_000) - val presenter = createVoiceMessagePresenter(fakeMediaLoader, fakeVoiceCache, content) + fun `pressing play downloads and plays`() = runTest { + val presenter = createVoiceMessagePresenter( + content = aTimelineItemVoiceContent(durationMs = 2_000), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -68,6 +62,11 @@ class VoiceMessagePresenterTest { initialState.eventSink(VoiceMessageEvents.PlayPause) + awaitItem().also { + Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Downloading) + Truth.assertThat(it.progress).isEqualTo(0f) + Truth.assertThat(it.time).isEqualTo("0:02") + } awaitItem().also { Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Pause) Truth.assertThat(it.progress).isEqualTo(0.5f) @@ -77,22 +76,18 @@ class VoiceMessagePresenterTest { } @Test - fun `pressing play with file not in cache downloads it but fails`() = runTest { - fakeMediaLoader.apply { - shouldFail = true - } - fakeVoiceCache.apply { - givenIsInCache(false) - givenMoveToCache(true) - } - val presenter = createVoiceMessagePresenter(fakeMediaLoader, fakeVoiceCache) + fun `pressing play downloads and fails`() = runTest { + val presenter = createVoiceMessagePresenter( + voiceMessageMediaRepo = FakeVoiceMessageMediaRepo().apply { shouldFail = true }, + content = aTimelineItemVoiceContent(durationMs = 2_000), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem().also { Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Play) Truth.assertThat(it.progress).isEqualTo(0f) - Truth.assertThat(it.time).isEqualTo("1:01") + Truth.assertThat(it.time).isEqualTo("0:02") } initialState.eventSink(VoiceMessageEvents.PlayPause) @@ -100,59 +95,21 @@ class VoiceMessagePresenterTest { awaitItem().also { Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Downloading) Truth.assertThat(it.progress).isEqualTo(0f) - Truth.assertThat(it.time).isEqualTo("1:01") + Truth.assertThat(it.time).isEqualTo("0:02") } - awaitItem().also { Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Retry) Truth.assertThat(it.progress).isEqualTo(0f) - Truth.assertThat(it.time).isEqualTo("1:01") + Truth.assertThat(it.time).isEqualTo("0:02") } } } @Test - fun `pressing play with file not in cache downloads it but then caching fails`() = runTest { - fakeMediaLoader.apply { - shouldFail = false - } - fakeVoiceCache.apply { - givenIsInCache(false) - givenMoveToCache(false) - } - val presenter = createVoiceMessagePresenter(fakeMediaLoader, fakeVoiceCache) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem().also { - Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Play) - Truth.assertThat(it.progress).isEqualTo(0f) - Truth.assertThat(it.time).isEqualTo("1:01") - } - - initialState.eventSink(VoiceMessageEvents.PlayPause) - - awaitItem().also { - Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Downloading) - Truth.assertThat(it.progress).isEqualTo(0f) - Truth.assertThat(it.time).isEqualTo("1:01") - } - - awaitItem().also { - Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Retry) - Truth.assertThat(it.progress).isEqualTo(0f) - Truth.assertThat(it.time).isEqualTo("1:01") - } - } - } - - @Test - fun `acquire control then play then play and pause while having control`() = runTest { - fakeVoiceCache.apply { - givenIsInCache(true) - } - val content = aTimelineItemVoiceContent(durationMs = 2_000) - val presenter = createVoiceMessagePresenter(fakeMediaLoader, fakeVoiceCache, content) + fun `pressing pause while playing pauses`() = runTest { + val presenter = createVoiceMessagePresenter( + content = aTimelineItemVoiceContent(durationMs = 2_000), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -163,78 +120,28 @@ class VoiceMessagePresenterTest { } initialState.eventSink(VoiceMessageEvents.PlayPause) + skipItems(1) // skip downloading state - awaitItem().also { + val playingState = awaitItem().also { Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Pause) Truth.assertThat(it.progress).isEqualTo(0.5f) Truth.assertThat(it.time).isEqualTo("0:01") } - initialState.eventSink(VoiceMessageEvents.PlayPause) - + playingState.eventSink(VoiceMessageEvents.PlayPause) awaitItem().also { Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Play) Truth.assertThat(it.progress).isEqualTo(0.5f) Truth.assertThat(it.time).isEqualTo("0:01") } - - initialState.eventSink(VoiceMessageEvents.PlayPause) - - awaitItem().also { - Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Pause) - Truth.assertThat(it.progress).isEqualTo(1.0f) - Truth.assertThat(it.time).isEqualTo("0:02") - } - } - } - - @Test - fun `pressing play with file not in cache downloads it successfully`() = runTest { - fakeMediaLoader.apply { - shouldFail = false - } - fakeVoiceCache.apply { - givenIsInCache(false) - givenMoveToCache(true) - } - val content = aTimelineItemVoiceContent(durationMs = 2_000) - val presenter = createVoiceMessagePresenter(fakeMediaLoader, fakeVoiceCache, content) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem().also { - Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Play) - Truth.assertThat(it.progress).isEqualTo(0f) - Truth.assertThat(it.time).isEqualTo("0:02") - } - - initialState.eventSink(VoiceMessageEvents.PlayPause) - - awaitItem().also { - Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Downloading) - Truth.assertThat(it.progress).isEqualTo(0f) - Truth.assertThat(it.time).isEqualTo("0:02") - } - - awaitItem().also { - Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Pause) - Truth.assertThat(it.progress).isEqualTo(0.5f) - Truth.assertThat(it.time).isEqualTo("0:01") - } } } @Test fun `content with null eventId shows disabled button`() = runTest { - fakeMediaLoader.apply { - shouldFail = false - } - fakeVoiceCache.apply { - givenIsInCache(false) - givenMoveToCache(true) - } - val content = aTimelineItemVoiceContent(eventId = null) - val presenter = createVoiceMessagePresenter(fakeMediaLoader, fakeVoiceCache, content) + val presenter = createVoiceMessagePresenter( + content = aTimelineItemVoiceContent(eventId = null), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -248,12 +155,9 @@ class VoiceMessagePresenterTest { @Test fun `seeking seeks`() = runTest { - fakeVoiceCache.apply { - givenIsInCache(true) - } - val content = aTimelineItemVoiceContent(durationMs = 10_000) - - val presenter = createVoiceMessagePresenter(fakeMediaLoader, fakeVoiceCache, content) + val presenter = createVoiceMessagePresenter( + content = aTimelineItemVoiceContent(durationMs = 10_000), + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -265,6 +169,8 @@ class VoiceMessagePresenterTest { initialState.eventSink(VoiceMessageEvents.PlayPause) + skipItems(1) // skip downloading state + awaitItem().also { Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Pause) Truth.assertThat(it.progress).isEqualTo(0.1f) @@ -283,12 +189,18 @@ class VoiceMessagePresenterTest { } fun createVoiceMessagePresenter( - fakeMediaLoader: FakeMediaLoader, - voiceCacheFake: FakeVoiceMessageCache, + voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(), content: TimelineItemVoiceContent = aTimelineItemVoiceContent(), ) = VoiceMessagePresenter( - mediaLoader = fakeMediaLoader, - voiceMessagePlayerFactory = { eventId, mediaPath -> VoiceMessagePlayerImpl(FakeMediaPlayer(), eventId, mediaPath) }, - voiceMessageCacheFactory = { voiceCacheFake }, + voiceMessagePlayerFactory = { eventId, mediaSource, mimeType, body -> + DefaultVoiceMessagePlayer( + mediaPlayer = FakeMediaPlayer(), + voiceMessageMediaRepoFactory = { _, _, _ -> voiceMessageMediaRepo }, + eventId = eventId, + mediaSource = mediaSource, + mimeType = mimeType, + body = body + ) + }, content = content, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt index 9ef0413a3a..8ceb878adf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -24,6 +24,7 @@ import io.element.android.tests.testutils.simulateLongTask class FakeMediaLoader : MatrixMediaLoader { var shouldFail = false + var path: String = "" override suspend fun loadMediaContent(source: MediaSource): Result = simulateLongTask { if (shouldFail) { @@ -45,7 +46,7 @@ class FakeMediaLoader : MatrixMediaLoader { if (shouldFail) { Result.failure(RuntimeException()) } else { - Result.success(FakeMediaFile("")) + Result.success(FakeMediaFile(path)) } } } From e864871c0ea9b52cc2bad7d494304f042d6ba3f4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Oct 2023 13:10:25 +0200 Subject: [PATCH 180/281] PIN : Fix test compilation --- features/ftue/impl/build.gradle.kts | 1 + .../ftue/impl/DefaultFtueStateTests.kt | 4 ++ features/lockscreen/test/build.gradle.kts | 28 +++++++++++++ .../lockscreen/test/FakeLockScreenService.kt | 41 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 features/lockscreen/test/build.gradle.kts create mode 100644 features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index acdb97b517..1719aecbe1 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { testImplementation(projects.services.analytics.test) testImplementation(projects.libraries.permissions.impl) testImplementation(projects.libraries.permissions.test) + testImplementation(projects.features.lockscreen.test) testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) 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/DefaultFtueStateTests.kt index 1388eb8fc1..b84bd93d3b 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/DefaultFtueStateTests.kt @@ -23,6 +23,8 @@ import io.element.android.features.ftue.impl.migration.MigrationScreenStore import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState +import io.element.android.features.lockscreen.api.LockScreenService +import io.element.android.features.lockscreen.test.FakeLockScreenService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -186,6 +188,7 @@ class DefaultFtueStateTests { migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(), permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), matrixClient: MatrixClient = FakeMatrixClient(), + lockScreenService: LockScreenService = FakeLockScreenService(), sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, // First version where notification permission is required ) = DefaultFtueState( sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), @@ -194,6 +197,7 @@ class DefaultFtueStateTests { welcomeScreenState = welcomeState, migrationScreenStore = migrationScreenStore, permissionStateProvider = permissionStateProvider, + lockScreenService = lockScreenService, matrixClient = matrixClient, ) } diff --git a/features/lockscreen/test/build.gradle.kts b/features/lockscreen/test/build.gradle.kts new file mode 100644 index 0000000000..083b54b88b --- /dev/null +++ b/features/lockscreen/test/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.features.lockscreen.test" +} + +dependencies { + implementation(libs.coroutines.core) + api(projects.features.lockscreen.api) +} diff --git a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt new file mode 100644 index 0000000000..50581de69b --- /dev/null +++ b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt @@ -0,0 +1,41 @@ +/* + * 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.lockscreen.test + +import io.element.android.features.lockscreen.api.LockScreenLockState +import io.element.android.features.lockscreen.api.LockScreenService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeLockScreenService() : LockScreenService { + + private var isSetupRequired: Boolean = false + private val _lockState: MutableStateFlow = MutableStateFlow(LockScreenLockState.Locked) + override val lockState: StateFlow = _lockState + + override suspend fun isSetupRequired(): Boolean { + return isSetupRequired + } + + fun setIsSetupRequired(isSetupRequired: Boolean) { + this.isSetupRequired = isSetupRequired + } + + fun setLockState(lockState: LockScreenLockState) { + _lockState.value = lockState + } +} From a7c83a94d3e592952561d9ba031bb92db86280d6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Oct 2023 14:39:13 +0200 Subject: [PATCH 181/281] PIN : Fix SetupPinPresenterTest --- .../lockscreen/impl/setup/SetupPinPresenterTest.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 81c37e6d69..5b6e76fb06 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -30,6 +30,7 @@ import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import org.junit.Test @@ -77,7 +78,9 @@ class SetupPinPresenterTest { assertThat(state.setupPinFailure).isNull() state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } - awaitLastSequentialItem().also { state -> + consumeItemsUntilPredicate { + it.isConfirmationStep + }.last().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() @@ -96,7 +99,9 @@ class SetupPinPresenterTest { assertThat(state.setupPinFailure).isNull() state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } - awaitLastSequentialItem().also { state -> + consumeItemsUntilPredicate { + it.isConfirmationStep + }.last().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() From 3ec62ad58a3454eef23e44226662277a1c653286 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 26 Oct 2023 14:55:23 +0200 Subject: [PATCH 182/281] Use Float instead of Double for all the level metering logic. (#1645) This is in preparation of further changes to the way the audio level is computed and to allow recording and sending of the waveform. The main reasoning behind the change is twofold: 1) We don't need the precision of Double in our context (we just need a rough indication of the changes in audio level to successfully draw a level meter or a waveform in our UI). 2) Performance: It is true that on 64 bit CPUs single operations involving Floats or Doubles take the same amount of time (i.e one clock cycle). But there are other aspects here that vouch in favor of Floats: - A float takes half the space in memory compared to a double, so when storing long lists of them this can add up. - On Android O and greater the ART runtime can "vectorize" certain operations on lists and make use of the CPU's SIMD registers which are generally 128 bits. So by using floats 4 of them can fit and be computed at the same time whilst with doubles only 2 will fit halving the throughput. References: - https://source.android.com/docs/core/runtime/improvements - https://www.slideshare.net/linaroorg/automatic-vectorization-in-art-android-runtime-sfo17216 --- .../composer/VoiceMessageComposerStateProvider.kt | 2 +- .../voicemessages/VoiceMessageComposerPresenterTest.kt | 2 +- .../android/libraries/textcomposer/TextComposer.kt | 2 +- .../textcomposer/components/VoiceMessageRecording.kt | 8 ++++---- .../libraries/textcomposer/model/VoiceMessageState.kt | 2 +- .../libraries/voicerecorder/api/VoiceRecorderState.kt | 2 +- .../libraries/voicerecorder/impl/VoiceRecorderImpl.kt | 2 +- .../voicerecorder/impl/audio/AudioLevelCalculator.kt | 2 +- .../impl/audio/DecibelAudioLevelCalculator.kt | 4 ++-- .../voicerecorder/impl/VoiceRecorderImplTest.kt | 10 +++++----- .../voicerecorder/test/FakeAudioLevelCalculator.kt | 6 +++--- .../libraries/voicerecorder/test/FakeVoiceRecorder.kt | 2 +- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt index 01414a6dda..c448ca1a84 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt @@ -23,7 +23,7 @@ import kotlin.time.Duration.Companion.seconds internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, level = 0.5)), + aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, level = 0.5f)), ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt index 58a7f54d48..ce965f32dc 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt @@ -63,7 +63,7 @@ class VoiceMessageComposerPresenterTest { companion object { private val RECORDING_DURATION = 1.seconds - private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, 0.2) + private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, 0.2f) } @Test 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 6e774bcda4..abb81fc090 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 @@ -778,7 +778,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { enableVoiceMessages = true, ) PreviewColumn(items = persistentListOf({ - VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5)) + VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5f)) }, { VoicePreview(voiceMessageState = VoiceMessageState.Preview) }, { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 99a0e82b7c..22244cd58b 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -42,7 +42,7 @@ import kotlin.time.Duration.Companion.seconds @Composable internal fun VoiceMessageRecording( - level: Double, + level: Float, duration: Duration, modifier: Modifier = Modifier, ) { @@ -79,7 +79,7 @@ internal fun VoiceMessageRecording( @Composable private fun DebugAudioLevel( - level: Double, + level: Float, modifier: Modifier = Modifier, ) { Box( @@ -89,7 +89,7 @@ private fun DebugAudioLevel( Box( modifier = Modifier .align(Alignment.CenterEnd) - .fillMaxWidth(level.toFloat()) + .fillMaxWidth(level) .background(ElementTheme.colors.iconQuaternary, shape = MaterialTheme.shapes.small) .fillMaxHeight() ) @@ -108,5 +108,5 @@ private fun RedRecordingDot( @PreviewsDayNight @Composable internal fun VoiceMessageRecordingPreview() = ElementPreview { - VoiceMessageRecording(0.5, 0.seconds) + VoiceMessageRecording(0.5f, 0.seconds) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index 590d5f2e50..fce53e2af4 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -25,6 +25,6 @@ sealed class VoiceMessageState { data object Sending: VoiceMessageState() data class Recording( val duration: Duration, - val level: Double, + val level: Float, ): VoiceMessageState() } diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt index 6ba1476ac7..6f40677beb 100644 --- a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt @@ -31,7 +31,7 @@ sealed class VoiceRecorderState { * @property elapsedTime The elapsed time since the recording started. * @property level The current audio level of the recording as a fraction of 1. */ - data class Recording(val elapsedTime: Duration, val level: Double) : VoiceRecorderState() + data class Recording(val elapsedTime: Duration, val level: Float) : VoiceRecorderState() /** * The recorder has finished recording. diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt index b150cd1059..f8490c6e2a 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -98,7 +98,7 @@ class VoiceRecorderImpl @Inject constructor( } is Audio.Error -> { Timber.e("Voice message error: code=${audio.audioRecordErrorCode}") - _state.emit(VoiceRecorderState.Recording(elapsedTime, 0.0)) + _state.emit(VoiceRecorderState.Recording(elapsedTime, 0.0f)) } } } diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt index 554b6ba4b1..efcb2f5570 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt @@ -24,5 +24,5 @@ interface AudioLevelCalculator { * * @return A value between 0 and 1. */ - fun calculateAudioLevel(buffer: ShortArray): Double + fun calculateAudioLevel(buffer: ShortArray): Float } diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt index 8a16acf83b..045e6f0e0b 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt @@ -29,7 +29,7 @@ class DecibelAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { private const val REFERENCE_DB = 50.0 // Reference dB for normal conversation } - override fun calculateAudioLevel(buffer: ShortArray): Double { + override fun calculateAudioLevel(buffer: ShortArray): Float { val rms = buffer.rootMeanSquare() // Convert to decibels and clip @@ -37,7 +37,7 @@ class DecibelAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { val clipped = min(db, REFERENCE_DB) // Scale to the range [0.0, 1.0] - return clipped / REFERENCE_DB + return (clipped / REFERENCE_DB).toFloat() } private fun ShortArray.rootMeanSquare(): Double { diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt index 5c9e0506ab..3c45771cd2 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt @@ -60,11 +60,11 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) voiceRecorder.startRecord() - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, 1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, 1.0f)) timeSource += 1.seconds - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds,0.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds,0.0f)) timeSource += 1.seconds - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, 1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, 1.0f)) } } @@ -75,9 +75,9 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) voiceRecorder.startRecord() - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.minutes, 1.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.minutes, 1.0f)) timeSource += 29.minutes - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, 0.0)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, 0.0f)) timeSource += 1.minutes assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Finished(File(FILE_PATH), "audio/ogg")) diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt index 1615067f6c..90a5735a5f 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/test/FakeAudioLevelCalculator.kt @@ -19,8 +19,8 @@ package io.element.android.libraries.voicerecorder.test import io.element.android.libraries.voicerecorder.impl.audio.AudioLevelCalculator import kotlin.math.abs -class FakeAudioLevelCalculator: AudioLevelCalculator { - override fun calculateAudioLevel(buffer: ShortArray): Double { - return buffer.map { abs(it.toDouble()) }.average() / Short.MAX_VALUE +class FakeAudioLevelCalculator : AudioLevelCalculator { + override fun calculateAudioLevel(buffer: ShortArray): Float { + return buffer.map { abs(it.toFloat()) }.average().toFloat() / Short.MAX_VALUE } } diff --git a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt index 04153f3a9a..aa2e1e5e0d 100644 --- a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt +++ b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt @@ -29,7 +29,7 @@ import kotlin.time.TestTimeSource class FakeVoiceRecorder( private val timeSource: TestTimeSource = TestTimeSource(), private val recordingDuration: Duration = 0.seconds, - private val levels: List = listOf(0.1, 0.2) + private val levels: List = listOf(0.1f, 0.2f) ) : VoiceRecorder { private val _state = MutableStateFlow(VoiceRecorderState.Idle) override val state: StateFlow = _state From ca6a30d6cc1a10c8049084957743c56537da29a3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Oct 2023 15:25:40 +0200 Subject: [PATCH 183/281] PIN : fix quality --- .../android/appconfig/LockScreenConfig.kt | 1 - .../ftue/impl/state/DefaultFtueState.kt | 2 +- .../impl/DefaultLockScreenService.kt | 3 ++- .../lockscreen/impl/LockScreenFlowNode.kt | 3 ++- .../impl/pin/DefaultPinCodeManagerCallback.kt | 25 +++++++++++++++++++ .../lockscreen/impl/pin/PinCodeManager.kt | 6 ++--- .../settings/LockScreenSettingsFlowNode.kt | 3 ++- .../lockscreen/impl/unlock/PinUnlockState.kt | 5 +++- .../src/main/res/values-cs/translations.xml | 4 +++ .../src/main/res/values-de/translations.xml | 4 +++ .../src/main/res/values-es/translations.xml | 4 +++ .../src/main/res/values-fr/translations.xml | 4 +++ .../src/main/res/values-it/translations.xml | 4 +++ .../src/main/res/values-ro/translations.xml | 4 +++ .../src/main/res/values-ru/translations.xml | 4 +++ .../src/main/res/values-sk/translations.xml | 10 +++++++- .../main/res/values-zh-rTW/translations.xml | 4 +++ .../impl/setup/SetupPinPresenterTest.kt | 3 ++- .../impl/unlock/PinUnlockPresenterTest.kt | 9 ++++--- .../lockscreen/test/FakeLockScreenService.kt | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../src/main/res/values-ru/translations.xml | 2 +- .../src/main/res/values-sk/translations.xml | 2 +- .../main/res/values-zh-rTW/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-sk/translations.xml | 14 ++++++++--- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../src/main/res/values-ru/translations.xml | 2 +- .../src/main/res/values-sk/translations.xml | 3 ++- .../main/res/values-zh-rTW/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../src/main/res/values-ru/translations.xml | 2 +- .../src/main/res/values-sk/translations.xml | 2 +- .../main/res/values-zh-rTW/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../impl/KeyStoreSecretKeyProvider.kt | 4 ++- .../main/res/values-zh-rTW/translations.xml | 1 + .../src/main/res/values-sk/translations.xml | 3 +++ .../src/main/res/values/localazy.xml | 1 + 49 files changed, 131 insertions(+), 45 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt create mode 100644 features/lockscreen/impl/src/main/res/values-cs/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-de/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-es/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-fr/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-it/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-ro/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-ru/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index e73b3cd150..5f72bc6f86 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -63,5 +63,4 @@ object LockScreenConfigModule { maxPinCodeAttemptsBeforeLogout = 3, gracePeriodInMillis = 90_000L ) - } 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/DefaultFtueState.kt index 07d2372344..7d80fd7413 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/DefaultFtueState.kt @@ -93,7 +93,7 @@ class DefaultFtueState @Inject constructor( { shouldAskNotificationPermissions() }, { needsAnalyticsOptIn() }, { shouldDisplayLockscreenSetup() }, - ).any { it -> it() } + ).any { it() } } private fun shouldDisplayMigrationScreen(): Boolean { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt index 32e1e3d1b9..af1c40ffb7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenService +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn @@ -53,7 +54,7 @@ class DefaultLockScreenService @Inject constructor( private var lockJob: Job? = null init { - pinCodeManager.addCallback(object : PinCodeManager.Callback { + pinCodeManager.addCallback(object : DefaultPinCodeManagerCallback() { override fun onPinCodeVerified() { _lockScreenState.value = LockScreenLockState.Unlocked } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index e10ca406fb..a5560a5179 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -30,6 +30,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.setup.SetupPinNode @@ -70,7 +71,7 @@ class LockScreenFlowNode @AssistedInject constructor( data object Settings : NavTarget } - private val pinCodeManagerCallback = object : PinCodeManager.Callback { + private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeCreated() { plugins().forEach { it.onSetupCompleted() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt new file mode 100644 index 0000000000..3ce8565cd2 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.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.features.lockscreen.impl.pin + +open class DefaultPinCodeManagerCallback : PinCodeManager.Callback { + override fun onPinCodeVerified() = Unit + + override fun onPinCodeCreated() = Unit + + override fun onPinCodeRemoved() = Unit +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index c214e533ab..21e7281dc8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -29,17 +29,17 @@ interface PinCodeManager { /** * Called when the pin code is verified. */ - fun onPinCodeVerified() = Unit + fun onPinCodeVerified() /** * Called when the pin code is created. */ - fun onPinCodeCreated() = Unit + fun onPinCodeCreated() /** * Called when the pin code is removed. */ - fun onPinCodeRemoved() = Unit + fun onPinCodeRemoved() } /** diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt index 63d1937317..a07607eea3 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -32,6 +32,7 @@ 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.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.setup.SetupPinNode import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode @@ -70,7 +71,7 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( data object Settings : NavTarget } - private val pinCodeManagerCallback = object : PinCodeManager.Callback { + private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeVerified() { backstack.newRoot(NavTarget.Settings) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 178e8692d1..29d246b21b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -27,5 +27,8 @@ data class PinUnlockState( val signOutAction: Async, val eventSink: (PinUnlockEvents) -> Unit ) { - val isSignOutPromptCancellable = (remainingAttempts.dataOrNull() ?: 0) > 0 + val isSignOutPromptCancellable = when (remainingAttempts) { + is Async.Success -> remainingAttempts.data > 0 + else -> true + } } diff --git a/features/lockscreen/impl/src/main/res/values-cs/translations.xml b/features/lockscreen/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..0f7f3decfb --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,4 @@ + + + "Odhlašování…" + diff --git a/features/lockscreen/impl/src/main/res/values-de/translations.xml b/features/lockscreen/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..9491e2e1a0 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,4 @@ + + + "Abmelden…" + diff --git a/features/lockscreen/impl/src/main/res/values-es/translations.xml b/features/lockscreen/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..5f7393df00 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "Cerrando sesión…" + diff --git a/features/lockscreen/impl/src/main/res/values-fr/translations.xml b/features/lockscreen/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..64944ff843 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,4 @@ + + + "Déconnexion…" + diff --git a/features/lockscreen/impl/src/main/res/values-it/translations.xml b/features/lockscreen/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..579346ed6e --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "Uscita in corso…" + diff --git a/features/lockscreen/impl/src/main/res/values-ro/translations.xml b/features/lockscreen/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..7cbd3ca512 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,4 @@ + + + "Deconectare în curs…" + diff --git a/features/lockscreen/impl/src/main/res/values-ru/translations.xml b/features/lockscreen/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..3ce820fc0c --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,4 @@ + + + "Выполняется выход…" + diff --git a/features/lockscreen/impl/src/main/res/values-sk/translations.xml b/features/lockscreen/impl/src/main/res/values-sk/translations.xml index 593542d81e..fd4d8f6da7 100644 --- a/features/lockscreen/impl/src/main/res/values-sk/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sk/translations.xml @@ -1,16 +1,24 @@ + + "Máte 3 pokusy na odomknutie" + "Nesprávny PIN kód. Máte ešte %1$d pokus" "Nesprávny PIN kód. Máte ešte %1$d pokusy" "Nesprávny PIN kód. Máte ešte %1$d pokusov" + "biometrické overenie" + "biometrické odomknutie" "Zabudli ste PIN?" "Zmeniť PIN kód" "Povoliť biometrické odomknutie" "Odstrániť PIN" "Ste si istí, že chcete odstrániť PIN?" "Odstrániť PIN?" + "Povoliť %1$s" + "Radšej použijem PIN" + "Ušetrite si čas a použite zakaždým %1$s na odomknutie aplikácie" "Vyberte PIN" "Potvrdiť PIN" "Z bezpečnostných dôvodov si nemôžete toto zvoliť ako svoj PIN kód." @@ -22,5 +30,5 @@ Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplik "PIN kódy sa nezhodujú" "Ak chcete pokračovať, musíte sa znovu prihlásiť a vytvoriť nový PIN kód." "Prebieha odhlasovanie" - "Máte 3 pokusy na odomknutie" + "Prebieha odhlasovanie…" diff --git a/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 0000000000..9d0c11fcd8 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,4 @@ + + + "正在登出…" + diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 5b6e76fb06..3969ea7c28 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -23,6 +23,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText @@ -45,7 +46,7 @@ class SetupPinPresenterTest { @Test fun `present - complete flow`() = runTest { val pinCodeCreated = CompletableDeferred() - val callback = object : PinCodeManager.Callback { + val callback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeCreated() { pinCodeCreated.complete(Unit) } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index e6ba51c537..234beae337 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -20,8 +20,9 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel @@ -42,7 +43,7 @@ class PinUnlockPresenterTest { @Test fun `present - success verify flow`() = runTest { val pinCodeVerified = CompletableDeferred() - val callback = object : PinCodeManager.Callback { + val callback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeCreated() { pinCodeVerified.complete(Unit) } @@ -82,7 +83,7 @@ class PinUnlockPresenterTest { @Test fun `present - failure verify flow`() = runTest { val pinCodeVerified = CompletableDeferred() - val callback = object : PinCodeManager.Callback { + val callback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeCreated() { pinCodeVerified.complete(Unit) } @@ -145,7 +146,7 @@ class PinUnlockPresenterTest { private suspend fun createPinUnlockPresenter( scope: CoroutineScope, - callback: PinCodeManager.Callback = object : PinCodeManager.Callback {}, + callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), ): PinUnlockPresenter { val pinCodeManager = aPinCodeManager().apply { addCallback(callback) diff --git a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt index 50581de69b..012c8e9a5c 100644 --- a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt +++ b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt @@ -21,7 +21,7 @@ import io.element.android.features.lockscreen.api.LockScreenService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class FakeLockScreenService() : LockScreenService { +class FakeLockScreenService : LockScreenService { private var isSetupRequired: Boolean = false private val _lockState: MutableStateFlow = MutableStateFlow(LockScreenLockState.Locked) diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index 3edf7d1003..0094479b12 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -35,8 +35,8 @@ "Na %2$s je momentálně vysoká poptávka po %1$s. Vraťte se do aplikace za pár dní a zkuste to znovu. Díky za trpělivost!" - "Vítá vás %1$s" "Jste v pořadníku!" "Jdete do toho!" "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." + "Vítá vás %1$s!" diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index ac7df075db..0e757190a9 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -34,8 +34,8 @@ "Derzeit besteht eine hohe Nachfrage nach %1$s auf %2$s. Kehre in ein paar Tagen zur App zurück und versuche es erneut. Danke für deine Geduld!" - "Willkommen bei %1$s!" "Du bist fast am Ziel." "Du bist dabei." "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." + "Willkommen bei %1$s!" diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index 3b7dae468c..7362aac3e8 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -34,8 +34,8 @@ "Il y a une forte demande pour %1$s sur %2$s à l’heure actuelle. Revenez sur l’application dans quelques jours et réessayez. Merci pour votre patience !" - "Bienvenue dans %1$s !" "Vous y êtes presque." "Vous y êtes." "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." + "Bienvenue dans %1$s !" diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index 2241c63ec9..49e701558a 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -34,8 +34,8 @@ "Există o cerere mare pentru %1$s pentru %2$s în acest moment. Reveniți la aplicație în câteva zile și încercați din nou. Vă mulțumim pentru răbdare!" - "Bun venit la %1$s" "Sunteți pe lista de așteptare" "Sunteți conectat!" "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." + "Bun venit la%1$s!" diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml index 733b0d93d2..1751364597 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -35,8 +35,8 @@ "В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова. Спасибо за терпение!" - "Добро пожаловать в %1$s!" "Почти готово!" "Вы зарегистрированы!" "Matrix — это открытая сеть для безопасной децентрализованной связи." + "Добро пожаловать в %1$s!" diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml index 31656d628a..ef7ba0d8dc 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -35,8 +35,8 @@ "Momentálne je veľký dopyt po %1$s na %2$s. Vráťte sa do aplikácie za pár dní a skúste to znova. Ďakujeme za trpezlivosť!" - "Vitajte v %1$s" "Ste na čakanej listine!" "Ste dnu!" "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." + "Vitajte v %1$s!" diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml index 45f10295f3..e482fcc9f1 100644 --- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml @@ -25,6 +25,6 @@ "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %1$s" "您即將在 %1$s 建立帳號" - "歡迎使用 %1$s!" "Matrix 是一個開放網路,為了安全、去中心化的通訊而生。" + "歡迎使用 %1$s!" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index c9797db5ac..2149318670 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -35,8 +35,8 @@ "There\'s a high demand for %1$s on %2$s at the moment. Come back to the app in a few days and try again. Thanks for your patience!" - "Welcome to %1$s!" "You’re almost there." "You\'re in." "Matrix is an open network for secure, decentralised communication." + "Welcome to %1$s!" diff --git a/features/logout/api/src/main/res/values-sk/translations.xml b/features/logout/api/src/main/res/values-sk/translations.xml index 2c334e9c3b..fcd78f2022 100644 --- a/features/logout/api/src/main/res/values-sk/translations.xml +++ b/features/logout/api/src/main/res/values-sk/translations.xml @@ -1,12 +1,18 @@ - "Prosím, počkajte na dokončenie tohto kroku a až potom sa odhláste." - "Vaše kľúče sa ešte stále zálohujú" "Ste si istí, že sa chcete odhlásiť?" "Odhlásiť sa" "Prebieha odhlasovanie…" - "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam." - "Uložili ste kľúč na obnovenie?" + "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam." + "Vypli ste zálohovanie" + "Keď ste sa odpojili od internetu, vaše kľúče sa ešte stále zálohovali. Pripojte sa znova k internetu, aby sa vaše kľúče mohli zálohovať pred odhlásením." + "Vaše kľúče sa ešte stále zálohujú" + "Pred odhlásením počkajte, kým sa to dokončí." + "Vaše kľúče sa ešte stále zálohujú" + "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam." + "Obnovenie nie je nastavené" + "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam." + "Uložili ste si kľúč na obnovenie?" "Odhlásiť sa" "Odhlásiť sa" diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index 7129031292..3bd09a0b76 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -30,7 +30,6 @@ "Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu." "Nastavení režimu se nezdařilo, zkuste to prosím znovu." "Všechny zprávy" - "Pouze zmínky a klíčová slova" "V této místnosti mě upozornit na" "Zobrazit méně" "Zobrazit více" @@ -40,4 +39,5 @@ "Zobrazit méně" "Držte pro nahrávání" "Nahrání média se nezdařilo, zkuste to prosím znovu." + "Pouze zmínky a klíčová slova" diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index d08c04a785..161868614c 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -29,7 +29,6 @@ "Fehler beim Wiederherstellen des Standardmodus. Bitte versuche es erneut." "Fehler beim Einstellen des Modus. Bitte versuche es erneut." "Alle Nachrichten" - "Nur Erwähnungen und Schlüsselwörter" "Benachrichtige mich in diesem Raum bei" "Weniger anzeigen" "Mehr anzeigen" @@ -38,4 +37,5 @@ "Emoji hinzufügen" "Weniger anzeigen" "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." + "Nur Erwähnungen und Schlüsselwörter" diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index ea6f387d1a..aed47e5ff8 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -29,7 +29,6 @@ "Échec de la restauration du mode par défaut, veuillez réessayer." "Échec de la configuration du mode, veuillez réessayer." "Tous les messages" - "Mentions et mots clés uniquement" "Dans ce salon, prévenez-moi pour" "Afficher moins" "Afficher plus" @@ -38,4 +37,5 @@ "Ajouter un émoji" "Afficher moins" "Échec du traitement des médias à télécharger, veuillez réessayer." + "Mentions et mots clés uniquement" diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml index 20bec7b1b3..16e4867196 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -30,7 +30,6 @@ "Nu s-a reușit restaurarea modului implicit, vă rugăm să încercați din nou." "Nu s-a reușit setarea modului, vă rugăm să încercați din nou." "Toate mesajele" - "Numai mențiuni și cuvinte cheie" "În această cameră, anunțați-mă pentru" "Afișați mai puțin" "Afișați mai mult" @@ -39,4 +38,5 @@ "Adăugați emoji" "Afișați mai puțin" "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." + "Numai mențiuni și cuvinte cheie" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index 4bb44db372..2ba01ab763 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -30,7 +30,6 @@ "Не удалось восстановить режим по умолчанию, попробуйте еще раз." "Не удалось настроить режим, попробуйте еще раз." "Все сообщения" - "Только упоминания и ключевые слова" "В этой комнате уведомить меня о" "Показать меньше" "Показать больше" @@ -40,4 +39,5 @@ "Показать меньше" "Удерживайте для записи" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." + "Только упоминания и ключевые слова" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index c9129faf08..7b89350ff8 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -14,6 +14,7 @@ "Anketa" "Formátovanie textu" "História správ v tejto miestnosti nie je momentálne k dispozícii" + "História správ nie je v tejto miestnosti k dispozícii. Ak chcete zobraziť históriu správ, overte toto zariadenie." "Nepodarilo sa získať údaje o používateľovi" "Chceli by ste ich pozvať späť?" "V tomto rozhovore ste sami" @@ -30,7 +31,6 @@ "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." "Nepodarilo sa nastaviť režim, skúste to prosím znova." "Všetky správy" - "Iba zmienky a kľúčové slová" "V tejto miestnosti ma upozorniť na" "Zobraziť menej" "Zobraziť viac" @@ -40,4 +40,5 @@ "Zobraziť menej" "Podržaním nahrajte" "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." + "Iba zmienky a kľúčové slová" diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml index 078d71bd9f..d3e9c9bce9 100644 --- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml @@ -19,11 +19,11 @@ "無法重設為預設模式,請再試一次。" "無法設定模式,請再試一次。" "所有訊息" - "僅限提及與關鍵字" "較少" "更多" "重傳" "無法傳送您的訊息" "新增表情符號" "較少" + "僅限提及與關鍵字" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 555ff574df..233a9ecbf4 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -30,7 +30,6 @@ "Failed restoring the default mode, please try again." "Failed setting the mode, please try again." "All messages" - "Mentions and Keywords only" "In this room, notify me for" "Show less" "Show more" @@ -40,4 +39,5 @@ "Show less" "Hold to record" "Failed processing media to upload, please try again." + "Mentions and Keywords only" 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 7c80ba3066..b17ef35e9b 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -36,7 +36,6 @@ "Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu." "Nastavení režimu se nezdařilo, zkuste to prosím znovu." "Všechny zprávy" - "Pouze zmínky a klíčová slova" "V této místnosti mě upozornit na" "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." @@ -47,4 +46,5 @@ "Opustit místnost" "Zabezpečení" "Téma" + "Pouze zmínky a klíčová slova" 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 9549709ef0..5f0d558820 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -35,7 +35,6 @@ "Fehler beim Wiederherstellen des Standardmodus. Bitte versuche es erneut." "Fehler beim Einstellen des Modus. Bitte versuche es erneut." "Alle Nachrichten" - "Nur Erwähnungen und Schlüsselwörter" "Benachrichtige mich in diesem Raum bei" "Sperren" "Gesperrte Benutzer können dir keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Du kannst sie jederzeit entsperren." @@ -46,4 +45,5 @@ "Raum verlassen" "Sicherheit" "Thema" + "Nur Erwähnungen und Schlüsselwörter" 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 7cf727dfb5..f899a58873 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -35,7 +35,6 @@ "Échec de la restauration du mode par défaut, veuillez réessayer." "Échec de la configuration du mode, veuillez réessayer." "Tous les messages" - "Mentions et mots clés uniquement" "Dans ce salon, prévenez-moi pour" "Bloquer" "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment." @@ -46,4 +45,5 @@ "Quitter le salon" "Sécurité" "Sujet" + "Mentions et mots clés uniquement" 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 7ec53722f4..958058432a 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -35,7 +35,6 @@ "Nu s-a reușit restaurarea modului implicit, vă rugăm să încercați din nou." "Nu s-a reușit setarea modului, vă rugăm să încercați din nou." "Toate mesajele" - "Numai mențiuni și cuvinte cheie" "În această cameră, anunțați-mă pentru" "Blocați" "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." @@ -46,4 +45,5 @@ "Părăsiți camera" "Securitate" "Subiect" + "Numai mențiuni și cuvinte cheie" 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 1b2a305d3a..920fb1a2f8 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -36,7 +36,6 @@ "Не удалось восстановить режим по умолчанию, попробуйте еще раз." "Не удалось настроить режим, попробуйте еще раз." "Все сообщения" - "Только упоминания и ключевые слова" "В этой комнате уведомить меня о" "Заблокировать" "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время." @@ -47,4 +46,5 @@ "Покинуть комнату" "Безопасность" "Тема" + "Только упоминания и ключевые слова" 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 8d28fe5fe5..5d6a4131b5 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -36,7 +36,6 @@ "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." "Nepodarilo sa nastaviť režim, skúste to prosím znova." "Všetky správy" - "Iba zmienky a kľúčové slová" "V tejto miestnosti ma upozorniť na" "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ť." @@ -47,4 +46,5 @@ "Opustiť miestnosť" "Bezpečnosť" "Téma" + "Iba zmienky a kľúčové slová" diff --git a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml index 4b82111269..71ed1df861 100644 --- a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml @@ -26,7 +26,6 @@ "無法重設為預設模式,請再試一次。" "無法設定模式,請再試一次。" "所有訊息" - "僅限提及與關鍵字" "封鎖" "封鎖使用者" "解除封鎖" @@ -34,4 +33,5 @@ "離開聊天室" "安全性" "主題" + "僅限提及與關鍵字" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index c88e2e43fa..833b06ca40 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -35,7 +35,6 @@ "Failed restoring the default mode, please try again." "Failed setting the mode, please try again." "All messages" - "Mentions and Keywords only" "In this room, notify me for" "Block" "Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime." @@ -46,4 +45,5 @@ "Leave room" "Security" "Topic" + "Mentions and Keywords only" diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt index 12be896a5b..9313e7a48b 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt @@ -40,7 +40,9 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider { // False positive lint issue @SuppressLint("WrongConstant") override fun getOrCreateKey(alias: String): SecretKey { - val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).also { it.load(null) } + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { + load(null) + } val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) ?.secretKey return if (secretKeyEntry == null) { diff --git a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml index 2453e2d825..0cee629b87 100644 --- a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml @@ -30,6 +30,7 @@ "%1$s 已被您移除" "%1$s 邀請 %2$s 加入聊天室" "您邀請 %1$s 加入聊天室" + "%1$s 撤銷了 %2$s 加入房間的邀請" "%1$s 將主題變更為 %2$s" "您將主題變更為 %1$s" "聊天室主題已被 %1$s 移除" 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 5cb20f8e22..ccefa660f2 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -167,6 +167,7 @@ "Video" "Hlasová správa" "Čaká sa…" + "Čaká sa na dešifrovací kľúč" "Ste si istí, že chcete ukončiť túto anketu?" "Anketa: %1$s" "Potvrdenie" @@ -273,6 +274,8 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""Zadať…" "Kľúč na obnovu potvrdený" "Potvrďte kľúč na obnovenie" + "Skopírovaný kľúč na obnovenie" + "Generovanie…" "Uložiť kľúč na obnovenie" "Zapíšte si kľúč na obnovenie na bezpečné miesto alebo ho uložte do správcu hesiel." "Ťuknutím skopírujte kľúč na obnovenie" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index be27e21ff4..dc8d916346 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -34,6 +34,7 @@ "Edit" "Enable" "End poll" + "Enter PIN" "Forgot password?" "Forward" "Invite" From b355d9fb6497a8bdee47a8908193836a0cc1982b Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Oct 2023 15:28:18 +0200 Subject: [PATCH 184/281] PIN: address PR review --- .../impl/components/PinEntryTextField.kt | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index e4869e71d8..019236aba7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -20,8 +20,10 @@ import androidx.compose.foundation.background 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.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -118,10 +120,19 @@ private fun PinDigitView( @Composable internal fun PinEntryTextFieldPreview() { ElementPreview { - PinEntryTextField( - pinEntry = PinEntry.createEmpty(4).fillWith("12"), - isSecured = true, - onValueChange = {}, - ) + val pinEntry = PinEntry.createEmpty(4).fillWith("12") + Column { + PinEntryTextField( + pinEntry = pinEntry, + isSecured = true, + onValueChange = {}, + ) + Spacer(modifier = Modifier.size(16.dp)) + PinEntryTextField( + pinEntry = pinEntry, + isSecured = false, + onValueChange = {}, + ) + } } } From 6d5ce881533665c28142e2366beda339fda456f8 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 26 Oct 2023 13:45:59 +0000 Subject: [PATCH 185/281] Update screenshots --- ...ents_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ents_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png index 28583d7fa6..6e3251fcd2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcd61b2ec311170adfde3dc4a5e73ab216d2c9030e0b5893ff7c518488d01bb2 -size 8103 +oid sha256:079fff99063ebce93c14b1c373ead6eba3a5952610c3c43429da597ae3a804b1 +size 11928 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png index 4899424130..70a36a4bf3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5b714f3dec62d10711e977e88d536b8bd78c60eabd1a718e2f70a160e6582c2 -size 8109 +oid sha256:db3f1674750094cef7b8e81a462ee4d2d180a2978dfa682f9db74bac5ad1a40d +size 11939 From 030e86f56b366e07182a47d1b448321b5c73ca03 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 26 Oct 2023 16:27:42 +0200 Subject: [PATCH 186/281] Update the chat screen UI using RoomInfo. (#1640) * Update the chat screen UI using `RoomInfo`. This is specially useful for getting live values for `hasRoomCall`. * Ensure the first `MatrixRoomInfo` is emitted ASAP * Try excluding `*Present$present$*` inner functions from kover as separate entities * Update strings --------- Co-authored-by: ElementBot --- build.gradle.kts | 2 + changelog.d/1158.feature | 1 + .../messages/impl/MessagesPresenter.kt | 23 ++++--- .../features/messages/impl/MessagesState.kt | 1 + .../messages/impl/MessagesStateProvider.kt | 4 ++ .../features/messages/impl/MessagesView.kt | 44 +++++++++++- .../messages/MessagesPresenterTest.kt | 8 ++- .../matrix/api/room/CurrentUserMembership.kt | 21 ++++++ .../libraries/matrix/api/room/MatrixRoom.kt | 3 + .../matrix/api/room/MatrixRoomInfo.kt | 43 ++++++++++++ .../libraries/matrix/impl/RustMatrixClient.kt | 4 +- .../matrix/impl/room/MatrixRoomInfoMapper.kt | 69 +++++++++++++++++++ .../matrix/impl/room/RustMatrixRoom.kt | 20 ++++++ .../matrix/impl/widget/RustWidgetDriver.kt | 6 +- .../matrix/test/room/FakeMatrixRoom.kt | 62 +++++++++++++++++ .../src/main/res/values/localazy.xml | 1 + ...sagesView-D-0_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...agesView-D-0_0_null_10,NEXUS_5,1.0,en].png | 3 + ...sagesView-D-0_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_7,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_8,NEXUS_5,1.0,en].png | 4 +- ...sagesView-D-0_0_null_9,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 +- ...agesView-N-0_1_null_10,NEXUS_5,1.0,en].png | 3 + ...sagesView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_4,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_5,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_6,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_7,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_8,NEXUS_5,1.0,en].png | 4 +- ...sagesView-N-0_1_null_9,NEXUS_5,1.0,en].png | 4 +- 36 files changed, 338 insertions(+), 52 deletions(-) create mode 100644 changelog.d/1158.feature create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CurrentUserMembership.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png diff --git a/build.gradle.kts b/build.gradle.kts index 970dfe8465..9cbf6cd3cc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -200,6 +200,7 @@ koverMerged { "*Node$*", // Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix SDK api, so it is not really relevant to unit test it: there is no logic to test. "io.element.android.libraries.matrix.impl.*", + "*Presenter\$present\$*" ) ) } @@ -252,6 +253,7 @@ koverMerged { excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" + excludes += "*Presenter\$present\$*" } bound { minValue = 85 diff --git a/changelog.d/1158.feature b/changelog.d/1158.feature new file mode 100644 index 0000000000..d2bf822649 --- /dev/null +++ b/changelog.d/1158.feature @@ -0,0 +1 @@ +Element Call: change the 'join call' button in a chat room when there's an active call. 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 adae018fee..ea9eade4b3 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 @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState 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 @@ -74,6 +75,7 @@ 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo @@ -113,6 +115,7 @@ class MessagesPresenter @AssistedInject constructor( @Composable override fun present(): MessagesState { + val roomInfo by room.roomInfoFlow.collectAsState(null) val localCoroutineScope = rememberCoroutineScope() val composerState = composerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present() @@ -125,14 +128,13 @@ class MessagesPresenter @AssistedInject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) val userHasPermissionToRedact by room.canRedactAsState(updateKey = syncUpdateFlow.value) - var roomName: Async by remember { mutableStateOf(Async.Uninitialized) } - var roomAvatar: Async by remember { mutableStateOf(Async.Uninitialized) } - LaunchedEffect(syncUpdateFlow.value) { - withContext(dispatchers.io) { - roomName = Async.Success(room.displayName) - roomAvatar = Async.Success(room.avatarData()) - } + val roomName: Async by remember { + derivedStateOf { roomInfo?.name?.let { Async.Success(it) } ?: Async.Uninitialized } } + val roomAvatar: Async by remember { + derivedStateOf { roomInfo?.avatarData()?.let { Async.Success(it) } ?: Async.Uninitialized } + } + var hasDismissedInviteDialog by rememberSaveable { mutableStateOf(false) } @@ -207,14 +209,15 @@ class MessagesPresenter @AssistedInject constructor( enableVoiceMessages = enableVoiceMessages, enableInRoomCalls = enableInRoomCalls, appName = buildMeta.applicationName, + isCallOngoing = roomInfo?.hasRoomCall ?: false, eventSink = { handleEvents(it) } ) } - private fun MatrixRoom.avatarData(): AvatarData { + private fun MatrixRoomInfo.avatarData(): AvatarData { return AvatarData( - id = roomId.value, - name = displayName, + id = id, + name = name, url = avatarUrl, size = AvatarSize.TimelineRoom ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 86784edc7a..5bce4f19a1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -50,6 +50,7 @@ data class MessagesState( val enableTextFormatting: Boolean, val enableVoiceMessages: Boolean, val enableInRoomCalls: Boolean, + val isCallOngoing: Boolean, val appName: String, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 9d12207362..275b324862 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -63,6 +63,9 @@ open class MessagesStateProvider : PreviewParameterProvider { attachmentsState = AttachmentsState.Sending.Uploading(0.33f) ), ), + aMessagesState().copy( + isCallOngoing = true, + ) ) } @@ -102,6 +105,7 @@ fun aMessagesState() = MessagesState( enableTextFormatting = true, enableVoiceMessages = true, enableInRoomCalls = true, + isCallOngoing = false, appName = "Element", eventSink = {} ) 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 3ff07def41..9ecf63dd29 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 @@ -21,17 +21,21 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement 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.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -94,6 +98,7 @@ import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import timber.log.Timber +import androidx.compose.material3.Button as Material3Button @Composable fun MessagesView( @@ -174,6 +179,7 @@ fun MessagesView( inRoomCallsEnabled = state.enableInRoomCalls, onBackPressed = onBackPressed, onRoomDetailsClicked = onRoomDetailsClicked, + isCallOngoing = state.isCallOngoing, onJoinCallClicked = onJoinCallClicked, ) } @@ -375,6 +381,7 @@ private fun MessagesViewTopBar( roomName: String?, roomAvatar: AvatarData?, inRoomCallsEnabled: Boolean, + isCallOngoing: Boolean, modifier: Modifier = Modifier, onRoomDetailsClicked: () -> Unit = {}, onJoinCallClicked: () -> Unit = {}, @@ -402,15 +409,48 @@ private fun MessagesViewTopBar( }, actions = { if (inRoomCallsEnabled) { - IconButton(onClick = onJoinCallClicked) { - Icon(CommonDrawables.ic_compound_video_call, contentDescription = null) // TODO add proper content description once we have the state + if (isCallOngoing) { + JoinCallMenuItem(onJoinCallClicked = onJoinCallClicked) + } else { + IconButton(onClick = onJoinCallClicked) { + Icon(CommonDrawables.ic_compound_video_call, contentDescription = stringResource(CommonStrings.a11y_start_call)) + } } } + Spacer(Modifier.width(8.dp)) }, windowInsets = WindowInsets(0.dp) ) } +@Composable +private fun JoinCallMenuItem( + modifier: Modifier = Modifier, + onJoinCallClicked: () -> Unit, +) { + Material3Button( + onClick = onJoinCallClicked, + colors = ButtonDefaults.buttonColors( + contentColor = ElementTheme.colors.bgCanvasDefault, + containerColor = ElementTheme.colors.iconAccentTertiary + ), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + modifier = modifier.heightIn(min = 36.dp), + ) { + Icon( + modifier = Modifier.size(20.dp), + resourceId = CommonDrawables.ic_compound_video_call, + contentDescription = null + ) + Spacer(Modifier.width(8.dp)) + Text( + text = stringResource(CommonStrings.action_join), + style = ElementTheme.typography.fontBodyMdMedium + ) + Spacer(Modifier.width(8.dp)) + } +} + @Composable private fun RoomAvatarAndNameRow( roomName: String, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index b5bbebfa33..190edd6173 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -68,6 +68,7 @@ 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.core.aBuildMeta 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.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaSender @@ -106,7 +107,8 @@ class MessagesPresenterTest { val initialState = consumeItemsUntilTimeout().last() assertThat(initialState.roomId).isEqualTo(A_ROOM_ID) assertThat(initialState.roomName).isEqualTo(Async.Success("")) - assertThat(initialState.roomAvatar).isEqualTo(Async.Success(AvatarData(id = A_ROOM_ID.value, name = "", size = AvatarSize.TimelineRoom))) + assertThat(initialState.roomAvatar) + .isEqualTo(Async.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom))) assertThat(initialState.userHasPermissionToSendMessage).isTrue() assertThat(initialState.userHasPermissionToRedact).isFalse() assertThat(initialState.hasNetworkConnection).isTrue() @@ -603,7 +605,9 @@ class MessagesPresenterTest { private fun TestScope.createMessagesPresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), - matrixRoom: MatrixRoom = FakeMatrixRoom(), + matrixRoom: MatrixRoom = FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo(id = roomId.value, name = "")) + }, navigator: FakeMessagesNavigator = FakeMessagesNavigator(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CurrentUserMembership.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CurrentUserMembership.kt new file mode 100644 index 0000000000..3aa71a9459 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CurrentUserMembership.kt @@ -0,0 +1,21 @@ +/* + * 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.api.room + +enum class CurrentUserMembership { + INVITED, JOINED, LEFT +} 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 4b7e4e4470..1cce3f03b7 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 @@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import java.io.Closeable import java.io.File @@ -51,6 +52,8 @@ interface MatrixRoom : Closeable { val activeMemberCount: Long val joinedMemberCount: Long + val roomInfoFlow: Flow + /** * A one-to-one is a room with exactly 2 members. * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt new file mode 100644 index 0000000000..6690387b04 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt @@ -0,0 +1,43 @@ +/* + * 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.api.room + +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem + +data class MatrixRoomInfo( + val id: String, + val name: String?, + val topic: String?, + val avatarUrl: String?, + val isDirect: Boolean, + val isPublic: Boolean, + val isSpace: Boolean, + val isTombstoned: Boolean, + val canonicalAlias: String?, + val alternativeAliases: List, + val currentUserMembership: CurrentUserMembership, + val latestEvent: EventTimelineItem?, + val inviter: RoomMember?, + val activeMembersCount: Long, + val invitedMembersCount: Long, + val joinedMembersCount: Long, + val highlightCount: Long, + val notificationCount: Long, + val userDefinedNotificationMode: RoomNotificationMode?, + val hasRoomCall: Boolean, + val activeRoomCallParticipants: List +) 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 21a489904b..67bbcb835c 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 @@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.impl.notification.RustNotificationSer import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.oidc.toRustAction import io.element.android.libraries.matrix.impl.pushers.RustPushersService +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 @@ -201,7 +202,8 @@ class RustMatrixClient constructor( systemClock = clock, roomContentForwarder = roomContentForwarder, sessionData = sessionStore.getSession(sessionId.value)!!, - roomSyncSubscriber = roomSyncSubscriber + roomSyncSubscriber = roomSyncSubscriber, + matrixRoomInfoMapper = MatrixRoomInfoMapper(), ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt new file mode 100644 index 0000000000..c24d996714 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt @@ -0,0 +1,69 @@ +/* + * 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.room + +import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.MatrixRoomInfo +import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper +import org.matrix.rustcomponents.sdk.use +import org.matrix.rustcomponents.sdk.Membership as RustMembership +import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo +import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode + +class MatrixRoomInfoMapper( + private val timelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(), +) { + + fun map(rustRoomInfo: RustRoomInfo): MatrixRoomInfo = rustRoomInfo.use { + return MatrixRoomInfo( + id = it.id, + name = it.name, + topic = it.topic, + avatarUrl = it.avatarUrl, + isDirect = it.isDirect, + isPublic = it.isPublic, + isSpace = it.isSpace, + isTombstoned = it.isTombstoned, + canonicalAlias = it.canonicalAlias, + alternativeAliases = it.alternativeAliases, + currentUserMembership = it.membership.map(), + latestEvent = it.latestEvent?.use (timelineItemMapper::map), + inviter = it.inviter?.use(RoomMemberMapper::map), + activeMembersCount = it.activeMembersCount.toLong(), + invitedMembersCount = it.invitedMembersCount.toLong(), + joinedMembersCount = it.joinedMembersCount.toLong(), + highlightCount = it.highlightCount.toLong(), + notificationCount = it.notificationCount.toLong(), + userDefinedNotificationMode = it.userDefinedNotificationMode?.map(), + hasRoomCall = it.hasRoomCall, + activeRoomCallParticipants = it.activeRoomCallParticipants + ) + } +} + +fun RustMembership.map(): CurrentUserMembership = when(this) { + RustMembership.INVITED -> CurrentUserMembership.INVITED + RustMembership.JOINED -> CurrentUserMembership.JOINED + RustMembership.LEFT -> CurrentUserMembership.LEFT +} + +fun RustRoomNotificationMode.map(): RoomNotificationMode = when(this) { + RustRoomNotificationMode.ALL_MESSAGES -> RoomNotificationMode.ALL_MESSAGES + RustRoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE +} 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 8dd0c02321..d45508e424 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 @@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind 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.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MessageEventType @@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.util.destroyAll +import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver import io.element.android.libraries.matrix.impl.widget.generateWidgetWebViewUrl import io.element.android.libraries.sessionstorage.api.SessionData @@ -59,12 +61,16 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.EventTimelineItem import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.RoomInfo +import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation @@ -73,6 +79,7 @@ import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown +import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File @@ -88,10 +95,23 @@ class RustMatrixRoom( private val roomContentForwarder: RoomContentForwarder, private val sessionData: SessionData, private val roomSyncSubscriber: RoomSyncSubscriber, + private val matrixRoomInfoMapper: MatrixRoomInfoMapper, ) : MatrixRoom { override val roomId = RoomId(innerRoom.id()) + override val roomInfoFlow: Flow = mxCallbackFlow { + launch { + val initial = innerRoom.roomInfo().use(matrixRoomInfoMapper::map) + channel.trySend(initial) + } + innerRoom.subscribeToRoomInfoUpdates(object : RoomInfoListener { + override fun call(roomInfo: RoomInfo) { + channel.trySend(matrixRoomInfoMapper.map(roomInfo)) + } + }) + } + // Create a dispatcher for all room methods... private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt index 2764cecfdc..33e829a206 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt @@ -68,7 +68,11 @@ class RustWidgetDriver( } override suspend fun send(message: String) { - driverAndHandle.handle.send(message) + try { + driverAndHandle.handle.send(message) + } catch (e: IllegalStateException) { + // The handle is closed, ignore + } } override fun close() { 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 c08a742391..844a049a40 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 @@ -29,16 +29,23 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.poll.PollKind +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.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService @@ -46,6 +53,8 @@ import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import java.io.File @@ -143,6 +152,9 @@ class FakeMatrixRoom( private var leaveRoomError: Throwable? = null + private val _roomInfoFlow: MutableSharedFlow = MutableStateFlow(aRoomInfo()) + override val roomInfoFlow: Flow = _roomInfoFlow + override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override val roomNotificationSettingsStateFlow: MutableStateFlow = @@ -497,6 +509,10 @@ class FakeMatrixRoom( fun givenGetWidgetDriverResult(result: Result) { getWidgetDriverResult = result } + + fun givenRoomInfo(roomInfo: MatrixRoomInfo) { + _roomInfoFlow.tryEmit(roomInfo) + } } data class SendLocationInvocation( @@ -523,3 +539,49 @@ data class EndPollInvocation( val pollStartId: EventId, val text: String, ) + +fun aRoomInfo( + id: String = A_ROOM_ID.value, + name: String? = A_ROOM_NAME, + topic: String? = "A topic", + avatarUrl: String? = AN_AVATAR_URL, + isDirect: Boolean = false, + isPublic: Boolean = true, + isSpace: Boolean = false, + isTombstoned: Boolean = false, + canonicalAlias: String? = null, + alternativeAliases: List = emptyList(), + currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, + latestEvent: EventTimelineItem? = null, + inviter: RoomMember? = null, + activeMembersCount: Long = 1, + invitedMembersCount: Long = 0, + joinedMembersCount: Long = 1, + highlightCount: Long = 0, + notificationCount: Long = 0, + userDefinedNotificationMode: RoomNotificationMode? = null, + hasRoomCall: Boolean = false, + activeRoomCallParticipants: List = emptyList() +) = MatrixRoomInfo( + id = id, + name = name, + topic = topic, + avatarUrl = avatarUrl, + isDirect = isDirect, + isPublic = isPublic, + isSpace = isSpace, + isTombstoned = isTombstoned, + canonicalAlias = canonicalAlias, + alternativeAliases = alternativeAliases, + currentUserMembership = currentUserMembership, + latestEvent = latestEvent, + inviter = inviter, + activeMembersCount = activeMembersCount, + invitedMembersCount = invitedMembersCount, + joinedMembersCount = joinedMembersCount, + highlightCount = highlightCount, + notificationCount = notificationCount, + userDefinedNotificationMode = userDefinedNotificationMode, + hasRoomCall = hasRoomCall, + activeRoomCallParticipants = activeRoomCallParticipants +) diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index c70eceb21f..85f499e404 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -11,6 +11,7 @@ "Ended poll" "Send files" "Show password" + "Start a call" "User menu" "Record voice message. Double tap and hold to record. Release to end recording." "Accept" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png index 6f1f867a7c..4b4c8a1c3c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f219bd9b9363f237e15bb73655dd53b2ec143e18c8544c11efbfa90390e091c -size 54312 +oid sha256:0eee7139a7675a08899accf49a97139b56333737e649e51ca1644ae6ae68d915 +size 54358 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png index 062dfb77f8..049ce0c7a1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6065f5330e1b3638719c6743db098bf6576fcffc6ccf8215f7f600a0b981144b -size 55731 +oid sha256:39e0b212a2cc423f3e33254d4a965c3071e88e5dd21268c42b3b555b1800b236 +size 55774 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6f2b4996d3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33afbe67d0e4156d615e6a58d4356386d5e2c0922cf646c2ec385338d869d8f5 +size 56023 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png index 1de7fbc725..3db6eef5cb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4bdafdfa50665f05ba5cd8749252e7e5d65bea8a8c6bfc1c46eb1acd1570b52 -size 56086 +oid sha256:42445e54ca2cfadbbd3f041f327825ea5595e27877376a760c37b746529d0980 +size 56135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png index d348a6f13f..143870a1b8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bfcc4dcaa8980cfc9c724e64a93116ac1fb81a1bf4421532cb196d4e07db7c5 -size 56024 +oid sha256:b9f1300da0ac7ef29286fb37d4489b38fa07632f71aa7d1f1cc0e0786749c626 +size 56124 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png index 224cef4b67..6c6e0a9ef4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c86a6da3d45b767a41f65e0cf4e8cacefb33e0409386a6233140891deb2258f6 -size 51965 +oid sha256:95f81914a56679419a7a6055040ae51b11616c87012310cbd221e3af41d55e8e +size 52039 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png index 82636b3e21..8d15129b08 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a1fc20759ff45eeef547621853b9684a663fe9dcdf78b316454dcae26d078f9 -size 52286 +oid sha256:ce97ccc29b01824ddd251dbe7756aa80749c0acc4fd8278cb1c3e4fc24c2337e +size 52334 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_7,NEXUS_5,1.0,en].png index b147d8489d..bfa886fbed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b65df67b1384aaa169f25d81e87072f2732c88676122e58e5d08850ada050ce -size 60073 +oid sha256:6d387f9678367ec0b8d6617a3ba378d309979af0e534e9f683805e71cd6c2794 +size 60172 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_8,NEXUS_5,1.0,en].png index 6ead9a5063..26057e68a3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abe449182a4cddbf11b9004579d27043e41c74d45261c1435d18b516946f9eed -size 42175 +oid sha256:0708776dd8884bd8975845c9fbb0b30fe6317d5692aaba36781fed5cff065c1b +size 42279 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_9,NEXUS_5,1.0,en].png index d28a44a2bd..9b5fc514e3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5deb0929efaab3160a7d0441bde9afe278064e8c0641cc62408bb25acbdced21 -size 41329 +oid sha256:766086d64f70b2a9f028149190d3f3c5b2ff34599907b38997657b6d093fa0f4 +size 41428 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png index 094ab2536a..6ae5a8b8e0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b9bc4654b6911d7f50b4b0242b650a529cb22c13a58caf5eae50366a2d27b37 -size 52532 +oid sha256:6d42806b454f15ed83e883bf5e2765d345163a57049b4b598c1b8d08458588dc +size 52624 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png index 32de8d07f9..a432f0e416 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e051668beb9030ff184d6e75831a00202854f6448880f399a7ee2d5be187740 -size 53880 +oid sha256:27f4fd957e004579b8b20373e1649f8c067a604595a5dcaae60b0caec7c43954 +size 53972 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b64203f138 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:010d76d537104b914a35cdb783c5cfcc1953a470d2d9bc23781c8837c9558f5f +size 54122 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png index 009ddeadaa..d5195bd919 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6c6603452db218811f3f5a2baa934ca640755b969e81ca8cbf6b3dcf663aee9 -size 54551 +oid sha256:397f061ceeefb2ea67a5d9a342050cea136554e936854bd26b92c3acfde45d74 +size 54646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png index d2f2f56d9b..578ad1301e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e953bc91e9959f1635d12416f6afde79427aeac4edef44fb38f1512672e544bd -size 51552 +oid sha256:a1a0280d6321b754e5e36d25eefc2d5c7192af16e52ae1304b4e7c57a7824e19 +size 51611 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png index 9437874ffd..ef03d74710 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0157713ae934c0771a5d3d4d064ce6dc9be11b1d8011f2360740d7f4a20f6f04 -size 50162 +oid sha256:99ded72c99dc1ba5b8c61cbacca6cc91f4b6325a16c12013150c6d63afd28182 +size 50273 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png index af4dae6214..5006c63543 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24f1d30bb62ed620671e87062a338fd53f409bce6e08fb7b116249decf74e409 -size 50295 +oid sha256:d2fc04a6c21e851b4e10c88ad9485f43f2f1576f5aeb40d4611513352341536e +size 50384 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png index 8e1be7bda7..ad11262fbe 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9457cbf3ff35a5677f983eba7d5087ca82e4d99a2bb942f64f5f7ccdb71e01b5 -size 54882 +oid sha256:5f3a764349e13ebe7205ff4d0b04a02f64c6eee5d65bb3b9cc62ac1d721d2d39 +size 54944 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_8,NEXUS_5,1.0,en].png index e41893a3b4..d9af4e88ee 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:549892647be61e9fd5fbbb80fa14734533e1b4628f95d409f2772dd990000f42 -size 38953 +oid sha256:459029e7871910e085378a40ff503d286b72fb7029c251d1e04a8a1f39cfd0ba +size 39017 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_9,NEXUS_5,1.0,en].png index 366b48f4ca..3f51c51648 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6bef454d8f57ba76b34b8588ba73635719feb6b1dde967b90f812d2410a7749 -size 38165 +oid sha256:a2d6cb8081da15f265d35e909c1d22bb6a48219162b20e1472230a698fdc2d2b +size 38231 From acd7aef6bea5fd16aea1e3883cb1cc967b689c16 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 26 Oct 2023 16:15:42 +0100 Subject: [PATCH 187/281] Move waveform UI to design system library (#1649) --- .../impl/timeline/components/event/TimelineItemVoiceView.kt | 4 ++-- .../libraries/designsystem/components/media}/Waveform.kt | 2 +- .../designsystem/components/media}/WaveformPlaybackView.kt | 2 +- ...ia_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png} | 0 ...ia_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename {features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline => libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media}/Waveform.kt (95%) rename {features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline => libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media}/WaveformPlaybackView.kt (99%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-D-50_50_null,NEXUS_5,1.0,en].png => ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-N-50_51_null,NEXUS_5,1.0,en].png => ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png} (100%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index b4c796c61b..34bdc75ca2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -44,8 +44,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider -import io.element.android.features.messages.impl.voicemessages.timeline.Waveform -import io.element.android.features.messages.impl.voicemessages.timeline.WaveformPlaybackView +import io.element.android.libraries.designsystem.components.media.Waveform +import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/Waveform.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/Waveform.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt index 708aee8b8e..4e86c87d18 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/Waveform.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.voicemessages.timeline +package io.element.android.libraries.designsystem.components.media import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt similarity index 99% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index 0924932271..d9d15c1c3b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.voicemessages.timeline +package io.element.android.libraries.designsystem.components.media import android.view.MotionEvent import androidx.compose.animation.core.animateFloatAsState diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-D-50_50_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-D-50_50_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-N-50_51_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.voicemessages.timeline_null_WaveformPlaybackView-N-50_51_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png From a67410f5739b1a7ab475f3a2e9d967dab2c16fa4 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 26 Oct 2023 16:33:58 +0100 Subject: [PATCH 188/281] Add voice message preview player (#1646) --------- Co-authored-by: ElementBot --- .../messagecomposer/MessageComposerView.kt | 6 ++ .../composer/VoiceMessageComposerEvents.kt | 4 + .../composer/VoiceMessageComposerPlayer.kt | 93 +++++++++++++++++++ .../composer/VoiceMessageComposerPresenter.kt | 28 +++++- .../messages/MessagesPresenterTest.kt | 3 + .../VoiceMessageComposerPresenterTest.kt | 92 ++++++++++++++++-- .../timeline/VoiceMessagePresenterTest.kt | 2 +- .../libraries/textcomposer/TextComposer.kt | 32 ++++++- .../components/VoiceMessagePreview.kt | 85 ++++++++++++++--- .../model/VoiceMessagePlayerEvent.kt | 26 ++++++ .../textcomposer/model/VoiceMessageState.kt | 4 +- .../impl/src/main/res/drawable/ic_pause.xml | 9 ++ .../impl/src/main/res/drawable/ic_play.xml | 9 ++ ...gePreview-D-15_15_null,NEXUS_5,1.0,en].png | 4 +- ...gePreview-N-15_16_null,NEXUS_5,1.0,en].png | 4 +- ...mposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 +- ...mposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 +- 17 files changed, 369 insertions(+), 40 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt rename features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/{ => composer}/VoiceMessageComposerPresenterTest.kt (82%) create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessagePlayerEvent.kt create mode 100644 libraries/textcomposer/impl/src/main/res/drawable/ic_pause.xml create mode 100644 libraries/textcomposer/impl/src/main/res/drawable/ic_play.xml 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 a2d8d9c571..0c71bdbec1 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 @@ -33,6 +33,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.TextComposer import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import kotlinx.coroutines.launch @Composable @@ -83,6 +84,10 @@ internal fun MessageComposerView( voiceMessageState.eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) } + val onVoicePlayerEvent = { event: VoiceMessagePlayerEvent -> + voiceMessageState.eventSink(VoiceMessageComposerEvents.PlayerEvent(event)) + } + TextComposer( modifier = modifier, state = state.richTextEditorState, @@ -98,6 +103,7 @@ internal fun MessageComposerView( enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, onVoiceRecordButtonEvent = onVoiceRecordButtonEvent, + onVoicePlayerEvent = onVoicePlayerEvent, onSendVoiceMessage = onSendVoiceMessage, onDeleteVoiceMessage = onDeleteVoiceMessage, onError = ::onError, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt index 384e249611..f80ee15d95 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt @@ -18,11 +18,15 @@ package io.element.android.features.messages.impl.voicemessages.composer import androidx.lifecycle.Lifecycle import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent sealed interface VoiceMessageComposerEvents { data class RecordButtonEvent( val pressEvent: PressEvent ): VoiceMessageComposerEvents + data class PlayerEvent( + val playerEvent: VoiceMessagePlayerEvent, + ): VoiceMessageComposerEvents data object SendVoiceMessage: VoiceMessageComposerEvents data object DeleteVoiceMessage: VoiceMessageComposerEvents data object AcceptPermissionRationale: VoiceMessageComposerEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt new file mode 100644 index 0000000000..976351fd62 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt @@ -0,0 +1,93 @@ +/* + * 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.voicemessages.composer + +import io.element.android.features.messages.impl.mediaplayer.MediaPlayer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +/** + * A media player for the voice message composer. + * + * @param mediaPlayer The [MediaPlayer] to use. + */ +class VoiceMessageComposerPlayer @Inject constructor( + private val mediaPlayer: MediaPlayer, +) { + private var lastPlayedMediaPath: String? = null + private val curPlayingMediaId + get() = mediaPlayer.state.value.mediaId + + val state: Flow = mediaPlayer.state.map { state -> + if (lastPlayedMediaPath == null || lastPlayedMediaPath != state.mediaId) { + return@map State.NotPlaying + } + + State( + isPlaying = state.isPlaying, + currentPosition = state.currentPosition + ) + }.distinctUntilChanged() + + /** + * Start playing from the current position. + * + * @param mediaPath The path to the media to be played. + * @param mimeType The mime type of the media file. + */ + fun play(mediaPath: String, mimeType: String) { + if (mediaPath == curPlayingMediaId) { + mediaPlayer.play() + } else { + lastPlayedMediaPath = mediaPath + mediaPlayer.acquireControlAndPlay( + uri = mediaPath, + mediaId = mediaPath, + mimeType = mimeType, + ) + } + } + + /** + * Pause playback. + */ + fun pause() { + if (lastPlayedMediaPath == curPlayingMediaId) { + mediaPlayer.pause() + } + } + + data class State( + /** + * Whether this player is currently playing. + */ + val isPlaying: Boolean, + /** + * The elapsed time of this player in milliseconds. + */ + val currentPosition: Long, + ) { + companion object { + val NotPlaying = State( + isPlaying = false, + currentPosition = 0L, + ) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index cbc5b7332f..51bc13847e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.voicemessages.composer import android.Manifest import androidx.compose.runtime.Composable 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 @@ -34,6 +35,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState @@ -50,6 +52,7 @@ class VoiceMessageComposerPresenter @Inject constructor( private val voiceRecorder: VoiceRecorder, private val analyticsService: AnalyticsService, private val mediaSender: MediaSender, + private val player: VoiceMessageComposerPlayer, permissionsPresenterFactory: PermissionsPresenter.Factory ) : Presenter { private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO) @@ -61,11 +64,14 @@ class VoiceMessageComposerPresenter @Inject constructor( val permissionState = permissionsPresenter.present() var isSending by remember { mutableStateOf(false) } + val playerState by player.state.collectAsState(initial = VoiceMessageComposerPlayer.State.NotPlaying) + val isPlaying by remember(playerState.isPlaying) { derivedStateOf { playerState.isPlaying } } val onLifecycleEvent = { event: Lifecycle.Event -> when (event) { Lifecycle.Event.ON_PAUSE -> { appCoroutineScope.finishRecording() + player.pause() } Lifecycle.Event.ON_DESTROY -> { appCoroutineScope.cancelRecording() @@ -99,6 +105,25 @@ class VoiceMessageComposerPresenter @Inject constructor( } } } + val onPlayerEvent = { event: VoiceMessagePlayerEvent -> + when (event) { + VoiceMessagePlayerEvent.Play -> + when (val recording = recorderState) { + is VoiceRecorderState.Finished -> + player.play( + mediaPath = recording.file.path, + mimeType = recording.mimeType, + ) + else -> Timber.e("Voice message player event received but no file to play") + } + VoiceMessagePlayerEvent.Pause -> { + player.pause() + } + is VoiceMessagePlayerEvent.Seek -> { + // TODO implement seeking + } + } + } val onAcceptPermissionsRationale = { permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog) @@ -131,6 +156,7 @@ class VoiceMessageComposerPresenter @Inject constructor( val handleEvents: (VoiceMessageComposerEvents) -> Unit = { event -> when (event) { is VoiceMessageComposerEvents.RecordButtonEvent -> onRecordButtonPress(event) + is VoiceMessageComposerEvents.PlayerEvent -> onPlayerEvent(event.playerEvent) is VoiceMessageComposerEvents.SendVoiceMessage -> localCoroutineScope.launch { onSendButtonPress() } @@ -150,7 +176,7 @@ class VoiceMessageComposerPresenter @Inject constructor( is VoiceRecorderState.Finished -> if (isSending) { VoiceMessageState.Sending } else { - VoiceMessageState.Preview + VoiceMessageState.Preview(isPlaying = isPlaying) } else -> VoiceMessageState.Idle }, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 190edd6173..6ab2e0d7f3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -40,8 +40,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.media.FakeLocalMediaFactory +import io.element.android.features.messages.mediaplayer.FakeMediaPlayer import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter @@ -633,6 +635,7 @@ class MessagesPresenterTest { FakeVoiceRecorder(), analyticsService, mediaSender, + player = VoiceMessageComposerPlayer(FakeMediaPlayer()), permissionsPresenterFactory, ) val timelinePresenter = TimelinePresenter( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt similarity index 82% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt rename to features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index ce965f32dc..90ab3ae16e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.features.messages.voicemessages +package io.element.android.features.messages.voicemessages.composer import android.Manifest import androidx.lifecycle.Lifecycle @@ -29,6 +29,8 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.VoiceMessageException +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer +import io.element.android.features.messages.mediaplayer.FakeMediaPlayer import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -37,6 +39,7 @@ import io.element.android.libraries.permissions.api.aPermissionsState import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService @@ -123,7 +126,63 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) val finalState = awaitItem() - assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState()) + voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - play recording before it is ready`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + val finalState = awaitItem().apply { + this.eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) + } + + // Nothing should happen + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(RECORDING_DURATION, 0.2f)) + voiceRecorder.assertCalls(started = 1, stopped = 0, deleted = 0) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - play recording`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) + val finalState = awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = true)) + } + voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) + + testPauseAndDestroy(finalState) + } + } + + @Test + fun `present - pause recording`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) + awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause)) + val finalState = awaitItem().also { + assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = false)) + } voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) testPauseAndDestroy(finalState) @@ -202,7 +261,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) awaitItem().apply { - assertThat(voiceMessageState).isEqualTo(VoiceMessageState.Preview) + assertThat(voiceMessageState).isEqualTo(aPreviewState()) eventSink(VoiceMessageComposerEvents.SendVoiceMessage) } @@ -232,7 +291,7 @@ class VoiceMessageComposerPresenterTest { assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending) ensureAllEventsConsumed() - assertThat(previewState.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + assertThat(previewState.voiceMessageState).isEqualTo(aPreviewState()) assertThat(matrixRoom.sendMediaCount).isEqualTo(0) mediaPreProcessor.givenAudioResult() @@ -393,17 +452,24 @@ class VoiceMessageComposerPresenterTest { VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE) ) - val onPauseState = when (mostRecentState.voiceMessageState) { + val onPauseState = when (val vmState = mostRecentState.voiceMessageState) { VoiceMessageState.Idle, - VoiceMessageState.Preview, VoiceMessageState.Sending -> { mostRecentState } is VoiceMessageState.Recording -> { - awaitItem().also { - assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Preview) + // If recorder was active, it stops + awaitItem().apply { + assertThat(voiceMessageState).isEqualTo(aPreviewState()) } } + is VoiceMessageState.Preview -> when(vmState.isPlaying) { + // If the preview was playing, it pauses + true -> awaitItem().apply { + assertThat(voiceMessageState).isEqualTo(aPreviewState()) + } + false -> mostRecentState + } } onPauseState.eventSink( @@ -415,7 +481,7 @@ class VoiceMessageComposerPresenterTest { VoiceMessageState.Sending -> ensureAllEventsConsumed() is VoiceMessageState.Recording, - VoiceMessageState.Preview -> + is VoiceMessageState.Preview -> assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) } } @@ -428,6 +494,7 @@ class VoiceMessageComposerPresenterTest { voiceRecorder, analyticsService, mediaSender, + player = VoiceMessageComposerPlayer(FakeMediaPlayer()), FakePermissionsPresenterFactory(permissionsPresenter), ) } @@ -445,4 +512,11 @@ class VoiceMessageComposerPresenterTest { initialState = initialPermissionState ) } + + private fun aPreviewState( + isPlaying: Boolean = false + ) = VoiceMessageState.Preview( + isPlaying = isPlaying + ) + } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt index 0a6ca3b8c4..3239c1879e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt @@ -145,7 +145,7 @@ class VoiceMessagePresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem().also { + awaitItem().also { Truth.assertThat(it.button).isEqualTo(VoiceMessageState.Button.Disabled) Truth.assertThat(it.progress).isEqualTo(0f) Truth.assertThat(it.time).isEqualTo("1:01") 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 abb81fc090..73c1764de2 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 @@ -74,6 +74,7 @@ import io.element.android.libraries.textcomposer.components.textInputRoundedCorn import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -99,6 +100,7 @@ fun TextComposer( onAddAttachment: () -> Unit = {}, onDismissTextFormatting: () -> Unit = {}, onVoiceRecordButtonEvent: (PressEvent) -> Unit = {}, + onVoicePlayerEvent: (VoiceMessagePlayerEvent) -> Unit = {}, onSendVoiceMessage: () -> Unit = {}, onDeleteVoiceMessage: () -> Unit = {}, onError: (Throwable) -> Unit = {}, @@ -108,6 +110,14 @@ fun TextComposer( onSendMessage(Message(html = html, markdown = state.messageMarkdown)) } + val onPlayVoiceMessageClicked = { + onVoicePlayerEvent(VoiceMessagePlayerEvent.Play) + } + + val onPauseVoiceMessageClicked = { + onVoicePlayerEvent(VoiceMessagePlayerEvent.Pause) + } + val layoutModifier = modifier .fillMaxSize() .height(IntrinsicSize.Min) @@ -179,10 +189,20 @@ fun TextComposer( val voiceRecording = @Composable { when (voiceMessageState) { - VoiceMessageState.Preview -> - VoiceMessagePreview(isInteractive = true) + is VoiceMessageState.Preview -> + VoiceMessagePreview( + isInteractive = true, + isPlaying = voiceMessageState.isPlaying, + onPlayClick = onPlayVoiceMessageClicked, + onPauseClick = onPauseVoiceMessageClicked + ) VoiceMessageState.Sending -> - VoiceMessagePreview(isInteractive = false) + VoiceMessagePreview( + isInteractive = false, + isPlaying = false, + onPlayClick = onPlayVoiceMessageClicked, + onPauseClick = onPauseVoiceMessageClicked + ) is VoiceMessageState.Recording -> VoiceMessageRecording(voiceMessageState.level, voiceMessageState.duration) VoiceMessageState.Idle -> {} @@ -191,7 +211,7 @@ fun TextComposer( val voiceDeleteButton = @Composable { val enabled = when (voiceMessageState) { - VoiceMessageState.Preview -> true + is VoiceMessageState.Preview -> true VoiceMessageState.Sending, is VoiceMessageState.Recording, VoiceMessageState.Idle -> false @@ -780,7 +800,9 @@ internal fun TextComposerVoicePreview() = ElementPreview { PreviewColumn(items = persistentListOf({ VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5f)) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Preview) + VoicePreview(voiceMessageState = VoiceMessageState.Preview(isPlaying = false)) + }, { + VoicePreview(voiceMessageState = VoiceMessageState.Preview(isPlaying = true)) }, { VoicePreview(voiceMessageState = VoiceMessageState.Sending) })) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 2164f8cd6f..4d8521834c 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -17,25 +17,36 @@ package io.element.android.libraries.textcomposer.components import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp 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 +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.textcomposer.R import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun VoiceMessagePreview( isInteractive: Boolean, + isPlaying: Boolean, modifier: Modifier = Modifier, + onPlayClick: () -> Unit = {}, + onPauseClick: () -> Unit = {} ) { Row( modifier = modifier @@ -44,28 +55,72 @@ internal fun VoiceMessagePreview( color = ElementTheme.colors.bgSubtleSecondary, shape = MaterialTheme.shapes.medium, ) - .padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) + .padding(start = 8.dp, end = 20.dp, top = 6.dp, bottom = 6.dp) .heightIn(26.dp), verticalAlignment = Alignment.CenterVertically, ) { - // TODO Replace with recording preview UI - Text( - text = "Finished recording", // Not localized because it is a placeholder - color = if (isInteractive) { - ElementTheme.colors.textSecondary - } else { - ElementTheme.colors.textDisabled - }, - style = ElementTheme.typography.fontBodySmMedium - ) + if (isPlaying) { + PlayerButton( + type = PlayerButtonType.Pause, + onClick = onPauseClick, + enabled = isInteractive, + ) + } else { + PlayerButton( + type = PlayerButtonType.Play, + onClick = onPlayClick, + enabled = isInteractive + ) + } + // TODO Add recording preview UI } } +private enum class PlayerButtonType { + Play, Pause +} + +@Composable +private fun PlayerButton( + type: PlayerButtonType, + enabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + onClick = onClick, + enabled = enabled, + modifier = modifier + .background(color = ElementTheme.colors.bgCanvasDefault, shape = CircleShape) + .size(30.dp.applyScaleUp()) + ) { + when (type) { + PlayerButtonType.Play -> PlayIcon() + PlayerButtonType.Pause -> PauseIcon() + } + } +} + +@Composable +private fun PauseIcon() = Icon( + resourceId = R.drawable.ic_pause, + contentDescription = stringResource(id = CommonStrings.a11y_pause), +) + +@Composable +private fun PlayIcon() = Icon( + resourceId = R.drawable.ic_play, + contentDescription = stringResource(id = CommonStrings.a11y_play), +) + @PreviewsDayNight @Composable internal fun VoiceMessagePreviewPreview() = ElementPreview { - Column { - VoiceMessagePreview(isInteractive = true) - VoiceMessagePreview(isInteractive = false) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + VoiceMessagePreview(isInteractive = true, isPlaying = true) + VoiceMessagePreview(isInteractive = true, isPlaying = false) + VoiceMessagePreview(isInteractive = false, isPlaying = false) } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessagePlayerEvent.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessagePlayerEvent.kt new file mode 100644 index 0000000000..7f827caef7 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessagePlayerEvent.kt @@ -0,0 +1,26 @@ +/* + * 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.textcomposer.model + +sealed class VoiceMessagePlayerEvent { + data object Play: VoiceMessagePlayerEvent() + data object Pause: VoiceMessagePlayerEvent() + + data class Seek( + val position: Float + ): VoiceMessagePlayerEvent() +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index fce53e2af4..012f655ad2 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -21,7 +21,9 @@ import kotlin.time.Duration sealed class VoiceMessageState { data object Idle: VoiceMessageState() - data object Preview: VoiceMessageState() + data class Preview( + val isPlaying: Boolean, + ): VoiceMessageState() data object Sending: VoiceMessageState() data class Recording( val duration: Duration, diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_pause.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000000..bc6deee55a --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_play.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_play.xml new file mode 100644 index 0000000000..0735689244 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_play.xml @@ -0,0 +1,9 @@ + + + diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png index 49fd301fe2..7fcecb3e63 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:256a21f9da287b5330fccf8de408137a39de1a73cefc5428e45cf391147fa249 -size 10955 +oid sha256:0ae485c12f93649418e661ed6f4a3f1393c3e1c703e8fce84e29ff9c4607ca7b +size 8699 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png index 607afb4819..013ce61eb0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d92dac54bc06086757108838beb2e4f2067fdc0525a1a71c3f92b7787fce128 -size 10261 +oid sha256:2afaaa054ed17809447352d245a287854afee0d05c0efd7b00341daf733dc07b +size 6623 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index 55bbca8ae9..1fc392e02d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:463aea9b16f9907af3acdc02d4c32e9f048b4a547e1079cd265e5138c759b461 -size 17917 +oid sha256:8eed5b8511637961df60be133688df984560cb84b333a9377e859446dfac2e04 +size 18036 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index 5e31ffc723..19603922a0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:240ae68a39f03df1cd46645d5fb5163c2fdb95ebe8083f8b18f72c118d5a2d25 -size 17030 +oid sha256:14bcc3395316251b2e25a23f95b53030bf2e2ebe0623ef7e113330a5fd2f853d +size 15852 From 00d24ce4b1e0c80cd2abd54de5722b871ac515ce Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 26 Oct 2023 17:37:24 +0200 Subject: [PATCH 189/281] Include waveform when sending voice messages (#1650) - New `AudioLevelCalculator` that outputs dB0v rescaled to the [0;1] range. - `VoiceRecorder` now stores the audio levels sampled while recording, then resamples them to 100 samples to use as waveform preview. - Waveform data is carried all the way as a `List` and converted to `List` in the [0;1024] range as per matrix spec only before sending it. --- .../composer/VoiceMessageComposerPresenter.kt | 12 +++- .../voicerecorder/api/VoiceRecorderState.kt | 2 + .../voicerecorder/impl/VoiceRecorderImpl.kt | 11 +++- .../impl/audio/AudioLevelCalculator.kt | 5 +- .../impl/audio/DBovAudioLevelCalculator.kt | 61 +++++++++++++++++++ .../impl/audio/DecibelAudioLevelCalculator.kt | 49 --------------- .../voicerecorder/impl/audio/Resample.kt | 38 ++++++++++++ .../impl/VoiceRecorderImplTest.kt | 18 +++++- ...est.kt => DBovAudioLevelCalculatorTest.kt} | 26 ++++---- .../voicerecorder/impl/audio/ResampleTest.kt | 41 +++++++++++++ .../voicerecorder/test/FakeVoiceRecorder.kt | 6 +- 11 files changed, 199 insertions(+), 70 deletions(-) create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculator.kt delete mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt create mode 100644 libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Resample.kt rename libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/{DecibelAudioLevelCalculatorTest.kt => DBovAudioLevelCalculatorTest.kt} (61%) create mode 100644 libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/ResampleTest.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index 51bc13847e..5e0d9bcada 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -148,6 +148,7 @@ class VoiceMessageComposerPresenter @Inject constructor( appCoroutineScope.sendMessage( file = finishedState.file, mimeType = finishedState.mimeType, + waveform = finishedState.waveform, ).invokeOnCompletion { isSending = false } @@ -207,12 +208,14 @@ class VoiceMessageComposerPresenter @Inject constructor( } private fun CoroutineScope.sendMessage( - file: File, mimeType: String, + file: File, + mimeType: String, + waveform: List ) = launch { val result = mediaSender.sendVoiceMessage( uri = file.toUri(), mimeType = mimeType, - waveForm = emptyList(), // TODO generate waveform + waveForm = waveform.toMSC3246range(), ) if (result.isFailure) { @@ -223,3 +226,8 @@ class VoiceMessageComposerPresenter @Inject constructor( voiceRecorder.deleteRecording() } } + +/** + * Resizes the given [0;1] float list to [0;1024] int list as per unstable MSC3246 spec. + */ +private fun List.toMSC3246range(): List = map { (it * 1024).toInt() } diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt index 6f40677beb..c168e3d5fe 100644 --- a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt @@ -38,9 +38,11 @@ sealed class VoiceRecorderState { * * @property file The recorded file. * @property mimeType The mime type of the file. + * @property waveform The waveform of the recording. */ data class Finished( val file: File, val mimeType: String, + val waveform: List, ) : VoiceRecorderState() } diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt index f8490c6e2a..e1481083f9 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig import io.element.android.libraries.voicerecorder.impl.audio.AudioLevelCalculator import io.element.android.libraries.voicerecorder.impl.audio.AudioReader import io.element.android.libraries.voicerecorder.impl.audio.Encoder +import io.element.android.libraries.voicerecorder.impl.audio.resample import io.element.android.libraries.voicerecorder.impl.file.VoiceFileConfig import io.element.android.libraries.voicerecorder.impl.file.VoiceFileManager import kotlinx.coroutines.CoroutineScope @@ -65,6 +66,7 @@ class VoiceRecorderImpl @Inject constructor( private var outputFile: File? = null private var audioReader: AudioReader? = null private var recordingJob: Job? = null + private val levels: MutableList = mutableListOf() private val _state = MutableStateFlow(VoiceRecorderState.Idle) override val state: StateFlow = _state @@ -74,6 +76,7 @@ class VoiceRecorderImpl @Inject constructor( Timber.i("Voice recorder started recording") outputFile = fileManager.createFile() .also(encoder::init) + levels.clear() val audioRecorder = audioReaderFactory.create(config, dispatchers).also { audioReader = it } @@ -94,6 +97,7 @@ class VoiceRecorderImpl @Inject constructor( is Audio.Data -> { val audioLevel = audioLevelCalculator.calculateAudioLevel(audio.buffer) _state.emit(VoiceRecorderState.Recording(elapsedTime, audioLevel)) + levels.add(audioLevel) encoder.encode(audio.buffer, audio.readSize) } is Audio.Error -> { @@ -124,12 +128,17 @@ class VoiceRecorderImpl @Inject constructor( if (cancelled) { deleteRecording() + levels.clear() } _state.emit( when (val file = outputFile) { null -> VoiceRecorderState.Idle - else -> VoiceRecorderState.Finished(file, fileConfig.mimeType) + else -> VoiceRecorderState.Finished( + file = file, + mimeType = fileConfig.mimeType, + waveform = levels.resample(100), + ) } ) } diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt index efcb2f5570..e38e1db040 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/AudioLevelCalculator.kt @@ -20,9 +20,8 @@ interface AudioLevelCalculator { /** * Calculate the audio level of the audio buffer. * - * @param buffer The audio buffer containing raw audio data. - * - * @return A value between 0 and 1. + * @param buffer The audio buffer containing 16bit PCM audio data. + * @return A float value between 0 and 1 proportional to the audio level. */ fun calculateAudioLevel(buffer: ShortArray): Float } diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculator.kt new file mode 100644 index 0000000000..0d63d99467 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculator.kt @@ -0,0 +1,61 @@ +/* + * 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.voicerecorder.impl.audio + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.RoomScope +import javax.inject.Inject +import kotlin.math.log10 +import kotlin.math.sqrt + +/** + * Default implementation of [AudioLevelCalculator]. + * + * It computes the normalized [0;1] dBov value of the given PCM16 encoded [ShortArray]. + * See: https://en.wikipedia.org/wiki/DBFS + */ +@ContributesBinding(RoomScope::class) +class DBovAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { + override fun calculateAudioLevel(buffer: ShortArray): Float { + return buffer.rms().dBov().normalize().coerceIn(0f, 1f) + } +} + +/** + * Computes the normalized (range 0.0 to 1.0) root mean square + * value of the given PCM16 encoded [ShortArray]. + */ +private fun ShortArray.rms(): Float { + val floats = FloatArray(this.size) { i -> this[i] / Short.MAX_VALUE.toFloat() } + val squared = FloatArray(this.size) { i -> floats[i] * floats[i] } + val sum = squared.fold(0.0f) { acc, f -> acc + f } + val average = sum / this.size + return sqrt(average) +} + +/** + * Converts the given RMS value to decibels relative to overload (dBov). + * It has range [-96.0, 0.0] where 0.0 is the value of a full scale square wave. + */ +private fun Float.dBov(): Float = 20 * log10(this) + +/** + * Normalizes the given dBov value to the range [0.0, 1.0]. + */ +private fun Float.normalize(): Float = (this + DYNAMIC_RANGE_PCM16) / DYNAMIC_RANGE_PCM16 + +private const val DYNAMIC_RANGE_PCM16: Float = 96.0f diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.kt deleted file mode 100644 index 045e6f0e0b..0000000000 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculator.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.libraries.voicerecorder.impl.audio - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.RoomScope -import javax.inject.Inject -import kotlin.math.log10 -import kotlin.math.min -import kotlin.math.sqrt - -@ContributesBinding(RoomScope::class) -class DecibelAudioLevelCalculator @Inject constructor() : AudioLevelCalculator { - companion object { - private const val REFERENCE_DB = 50.0 // Reference dB for normal conversation - } - - override fun calculateAudioLevel(buffer: ShortArray): Float { - val rms = buffer.rootMeanSquare() - - // Convert to decibels and clip - val db = 20 * log10(rms / REFERENCE_DB) - val clipped = min(db, REFERENCE_DB) - - // Scale to the range [0.0, 1.0] - return (clipped / REFERENCE_DB).toFloat() - } - - private fun ShortArray.rootMeanSquare(): Double { - // Use Double to avoid overflow - val sumOfSquares: Double = sumOf { it.toDouble() * it.toDouble() } - val avgSquare = sumOfSquares / size.toDouble() - return sqrt(avgSquare) - } -} diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Resample.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Resample.kt new file mode 100644 index 0000000000..49d3a1b521 --- /dev/null +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/Resample.kt @@ -0,0 +1,38 @@ +/* + * 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.voicerecorder.impl.audio + +/** + * Resamples [this] list to [size] using linear interpolation. + */ +fun List.resample(size: Int): List { + require(size > 0) + val input = this + if (input.isEmpty()) return List(size) { 0f } // fast path. + if (input.size == 1) return List(size) { input[0] } // fast path. + if (input.size == size) return this // fast path. + val step: Float = input.size.toFloat() / size.toFloat() + return buildList(size) { + for (i in 0 until size) { + val x0 = (i * step).toInt() + val x1 = (x0 + 1).coerceAtMost(input.size - 1) + val x = i * step - x0 + val y = input[x0] * (1 - x) + input[x1] * x + add(i, y) + } + } +} diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt index 3c45771cd2..cb9881087e 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt @@ -62,7 +62,7 @@ class VoiceRecorderImplTest { voiceRecorder.startRecord() assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, 1.0f)) timeSource += 1.seconds - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds,0.0f)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds, 0.0f)) timeSource += 1.seconds assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, 1.0f)) } @@ -80,7 +80,13 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, 0.0f)) timeSource += 1.minutes - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Finished(File(FILE_PATH), "audio/ogg")) + assertThat(awaitItem()).isEqualTo( + VoiceRecorderState.Finished( + file = File(FILE_PATH), + mimeType = "audio/ogg", + waveform = List(100) { 1f }, + ) + ) } } @@ -93,7 +99,13 @@ class VoiceRecorderImplTest { voiceRecorder.startRecord() skipItems(3) voiceRecorder.stopRecord() - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Finished(File(FILE_PATH), "audio/ogg")) + assertThat(awaitItem()).isEqualTo( + VoiceRecorderState.Finished( + file = File(FILE_PATH), + mimeType = "audio/ogg", + waveform = List(100) { 1f }, + ) + ) assertThat(fakeFileSystem.files[File(FILE_PATH)]).isEqualTo(ENCODED_DATA) } } diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculatorTest.kt similarity index 61% rename from libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt rename to libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculatorTest.kt index 8ffbf1ef8e..cb5404d6f2 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DecibelAudioLevelCalculatorTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DBovAudioLevelCalculatorTest.kt @@ -16,31 +16,35 @@ package io.element.android.libraries.voicerecorder.impl.audio +import com.google.common.truth.Truth import org.junit.Test -class DecibelAudioLevelCalculatorTest { +class DBovAudioLevelCalculatorTest { @Test - fun `given max values, it returns values within range`() { - val calculator = DecibelAudioLevelCalculator() + fun `given max values, it returns 1`() { + val calculator = DBovAudioLevelCalculator() val buffer = ShortArray(100) { Short.MAX_VALUE } val level = calculator.calculateAudioLevel(buffer) - assert(level in 0.0..1.0) + Truth.assertThat(level).isEqualTo(1.0f) } @Test fun `given mixed values, it returns values within range`() { - val calculator = DecibelAudioLevelCalculator() - val buffer = shortArrayOf(Short.MAX_VALUE, Short.MIN_VALUE, -1, 1) + val calculator = DBovAudioLevelCalculator() + val buffer = shortArrayOf(100, -200, 300, -400, 500, -600, 700, -800, 900, -1000) val level = calculator.calculateAudioLevel(buffer) - assert(level in 0.0..1.0) + Truth.assertThat(level).apply { + isGreaterThan(0f) + isLessThan(1f) + } } @Test - fun `given min values, it returns values within range`() { - val calculator = DecibelAudioLevelCalculator() - val buffer = ShortArray(100) { Short.MIN_VALUE } + fun `given min values, it returns 0`() { + val calculator = DBovAudioLevelCalculator() + val buffer = ShortArray(100) { 0 } val level = calculator.calculateAudioLevel(buffer) - assert(level in 0.0..1.0) + Truth.assertThat(level).isEqualTo(0.0f) } } diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/ResampleTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/ResampleTest.kt new file mode 100644 index 0000000000..1edec86018 --- /dev/null +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/audio/ResampleTest.kt @@ -0,0 +1,41 @@ +/* + * 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.voicerecorder.impl.audio + +import com.google.common.truth.Truth +import org.junit.Test + +class ResampleTest { + @Test + fun `resample works`() { + listOf(0.0f).resample(10).let { + Truth.assertThat(it).isEqualTo(listOf(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f)) + } + listOf(1.0f).resample(10).let { + Truth.assertThat(it).isEqualTo(listOf(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f)) + } + listOf(0.0f, 1.0f).resample(10).let { + Truth.assertThat(it).isEqualTo(listOf(0.0f, 0.2f, 0.4f, 0.6f, 0.8f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f)) + } + listOf(0.0f, 0.5f, 1.0f).resample(10).let { + Truth.assertThat(it).isEqualTo(listOf(0.0f, 0.15f, 0.3f, 0.45000002f, 0.6f, 0.75f, 0.90000004f, 1.0f, 1.0f, 1.0f)) + } + List(100) { it.toFloat() }.resample(10).let { + Truth.assertThat(it).isEqualTo(listOf(0.0f, 10.0f, 20.0f, 30.0f, 40.0f, 50.0f, 60.0f, 70.0f, 80.0f, 90.0f)) + } + } +} diff --git a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt index aa2e1e5e0d..7d3f140529 100644 --- a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt +++ b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt @@ -70,7 +70,11 @@ class FakeVoiceRecorder( _state.emit( when (curRecording) { null -> VoiceRecorderState.Idle - else -> VoiceRecorderState.Finished(curRecording!!, "audio/ogg") + else -> VoiceRecorderState.Finished( + file = curRecording!!, + mimeType = "audio/ogg", + waveform = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), + ) } ) } From cb8c6f3eb1d34c78dfc9f85124c5b2aa5da6ec4a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Oct 2023 10:32:38 +0200 Subject: [PATCH 190/281] Add RedIndicatorAtom --- .../atomic/atoms/RedIndicatorAtom.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RedIndicatorAtom.kt diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RedIndicatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RedIndicatorAtom.kt new file mode 100644 index 0000000000..c14f961a8f --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RedIndicatorAtom.kt @@ -0,0 +1,56 @@ +/* + * 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.atomic.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun RedIndicatorAtom( + modifier: Modifier = Modifier, + size: Dp = 10.dp, + borderSize: Dp = 1.dp, + color: Color = ElementTheme.colors.bgCriticalPrimary, +) { + Box( + modifier = modifier + .size(size) + .border(borderSize, ElementTheme.materialColors.background, CircleShape) + .padding(borderSize / 2) + .clip(CircleShape) + .background(color) + ) +} + +@PreviewsDayNight +@Composable +internal fun RedIndicatorAtomPreview() = ElementPreview { + RedIndicatorAtom() +} From e9e050a6d893053339849900d9da47688d01f151 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Oct 2023 16:23:14 +0200 Subject: [PATCH 191/281] Design kit: add destructive buttons. --- .../designsystem/theme/components/Button.kt | 120 ++++++++++++------ 1 file changed, 84 insertions(+), 36 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt index fce2cb3eff..b3bc9a01bd 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -64,6 +64,7 @@ fun Button( enabled: Boolean = true, size: ButtonSize = ButtonSize.Large, showProgress: Boolean = false, + destructive: Boolean = false, leadingIcon: IconSource? = null, ) = ButtonInternal( text = text, @@ -73,6 +74,7 @@ fun Button( enabled = enabled, size = size, showProgress = showProgress, + destructive = destructive, leadingIcon = leadingIcon ) @@ -84,6 +86,7 @@ fun OutlinedButton( enabled: Boolean = true, size: ButtonSize = ButtonSize.Large, showProgress: Boolean = false, + destructive: Boolean = false, leadingIcon: IconSource? = null, ) = ButtonInternal( text = text, @@ -93,6 +96,7 @@ fun OutlinedButton( enabled = enabled, size = size, showProgress = showProgress, + destructive = destructive, leadingIcon = leadingIcon ) @@ -104,6 +108,7 @@ fun TextButton( enabled: Boolean = true, size: ButtonSize = ButtonSize.Large, showProgress: Boolean = false, + destructive: Boolean = false, leadingIcon: IconSource? = null, ) = ButtonInternal( text = text, @@ -113,6 +118,7 @@ fun TextButton( enabled = enabled, size = size, showProgress = showProgress, + destructive = destructive, leadingIcon = leadingIcon ) @@ -122,7 +128,8 @@ private fun ButtonInternal( onClick: () -> Unit, style: ButtonStyle, modifier: Modifier = Modifier, - colors: ButtonColors = style.getColors(), + destructive: Boolean = false, + colors: ButtonColors = style.getColors(destructive), enabled: Boolean = true, size: ButtonSize = ButtonSize.Large, showProgress: Boolean = false, @@ -170,7 +177,12 @@ private fun ButtonInternal( ButtonStyle.Filled -> null ButtonStyle.Outlined -> BorderStroke( width = 1.dp, - color = ElementTheme.colors.borderInteractiveSecondary + color = if (destructive) + ElementTheme.colors.borderCriticalPrimary.copy( + alpha = if (enabled) 1f else 0.5f + ) + else + ElementTheme.colors.borderInteractiveSecondary ) ButtonStyle.Text -> null } @@ -246,26 +258,52 @@ internal enum class ButtonStyle { Filled, Outlined, Text; @Composable - fun getColors(): ButtonColors = when (this) { + fun getColors(destructive: Boolean): ButtonColors = when (this) { Filled -> ButtonDefaults.buttonColors( - containerColor = ElementTheme.materialColors.primary, + containerColor = getPrimaryColor(destructive), contentColor = ElementTheme.materialColors.onPrimary, - disabledContainerColor = ElementTheme.colors.bgActionPrimaryDisabled, + disabledContainerColor = if (destructive) { + ElementTheme.colors.bgCriticalPrimary.copy(alpha = 0.5f) + } else { + ElementTheme.colors.bgActionPrimaryDisabled + }, disabledContentColor = ElementTheme.colors.textOnSolidPrimary ) Outlined -> ButtonDefaults.buttonColors( containerColor = Color.Transparent, - contentColor = ElementTheme.materialColors.primary, + contentColor = getPrimaryColor(destructive), disabledContainerColor = Color.Transparent, - disabledContentColor = ElementTheme.colors.textDisabled, + disabledContentColor = getDisabledContentColor(destructive), ) Text -> ButtonDefaults.buttonColors( containerColor = Color.Transparent, - contentColor = if (LocalContentColor.current.isSpecified) LocalContentColor.current else ElementTheme.materialColors.primary, + contentColor = if (destructive) { + ElementTheme.colors.textCriticalPrimary + } else { + if (LocalContentColor.current.isSpecified) LocalContentColor.current else ElementTheme.materialColors.primary + }, disabledContainerColor = Color.Transparent, - disabledContentColor = ElementTheme.colors.textDisabled, + disabledContentColor = getDisabledContentColor(destructive), ) } + + @Composable + private fun getPrimaryColor(destructive: Boolean): Color { + return if (destructive) { + ElementTheme.colors.bgCriticalPrimary + } else { + ElementTheme.materialColors.primary + } + } + + @Composable + private fun getDisabledContentColor(destructive: Boolean): Color { + return if (destructive) { + ElementTheme.colors.textCriticalPrimary.copy(alpha = 0.5f) + } else { + ElementTheme.colors.textDisabled + } + } } @Preview(group = PreviewGroup.Buttons) @@ -326,7 +364,6 @@ internal fun TextButtonLargePreview() { private fun ButtonCombinationPreview( style: ButtonStyle, size: ButtonSize, - modifier: Modifier = Modifier, ) { ElementThemedPreview { Column( @@ -335,59 +372,70 @@ private fun ButtonCombinationPreview( .padding(16.dp) .width(IntrinsicSize.Max), ) { - // Normal - ButtonRowPreview( - modifier = Modifier.then(modifier), - style = style, - size = size, - ) - - // With icon - ButtonRowPreview( - modifier = Modifier.then(modifier), - leadingIcon = IconSource.Resource(CommonDrawables.ic_compound_share_android), - style = style, - size = size, - ) - - // With progress - ButtonRowPreview( - modifier = Modifier.then(modifier), - showProgress = true, - style = style, - size = size, - ) + ButtonMatrixPreview(style = style, size = size, destructive = false) + ButtonMatrixPreview(style = style, size = size, destructive = true) } } } +@Composable +private fun ButtonMatrixPreview( + style: ButtonStyle, + size: ButtonSize, + destructive: Boolean, +) { + // Normal + ButtonRowPreview( + style = style, + size = size, + destructive = destructive, + ) + // With icon + ButtonRowPreview( + leadingIcon = IconSource.Resource(CommonDrawables.ic_compound_share_android), + style = style, + size = size, + destructive = destructive, + ) + // With progress + ButtonRowPreview( + showProgress = true, + style = style, + size = size, + destructive = destructive, + ) +} + @Composable private fun ButtonRowPreview( style: ButtonStyle, size: ButtonSize, - modifier: Modifier = Modifier, leadingIcon: IconSource? = null, showProgress: Boolean = false, + destructive: Boolean = false, ) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + ) { ButtonInternal( text = "A button", showProgress = showProgress, + destructive = destructive, onClick = {}, style = style, size = size, leadingIcon = leadingIcon, - modifier = Modifier.then(modifier), ) ButtonInternal( text = "A button", showProgress = showProgress, + destructive = destructive, enabled = false, onClick = {}, style = style, size = size, leadingIcon = leadingIcon, - modifier = Modifier.then(modifier), ) } } From bec12c4cca7e5869dcac366585aacf8261be012a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 4 Oct 2023 16:31:34 +0200 Subject: [PATCH 192/281] Design kit: add destructive dialog action --- .../components/dialogs/ConfirmationDialog.kt | 4 +++ .../theme/components/AlertDialogContent.kt | 36 +++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt index 0c2fca84d5..801575fe56 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/ConfirmationDialog.kt @@ -38,6 +38,7 @@ fun ConfirmationDialog( title: String? = null, submitText: String = stringResource(id = CommonStrings.action_ok), cancelText: String = stringResource(id = CommonStrings.action_cancel), + destructiveSubmit: Boolean = false, thirdButtonText: String? = null, onCancelClicked: () -> Unit = onDismiss, onThirdButtonClicked: () -> Unit = {}, @@ -49,6 +50,7 @@ fun ConfirmationDialog( submitText = submitText, cancelText = cancelText, thirdButtonText = thirdButtonText, + destructiveSubmit = destructiveSubmit, onSubmitClicked = onSubmitClicked, onCancelClicked = onCancelClicked, onThirdButtonClicked = onThirdButtonClicked, @@ -67,6 +69,7 @@ private fun ConfirmationDialogContent( title: String? = null, thirdButtonText: String? = null, onThirdButtonClicked: () -> Unit = {}, + destructiveSubmit: Boolean = false, icon: @Composable (() -> Unit)? = null, ) { SimpleAlertDialogContent( @@ -79,6 +82,7 @@ private fun ConfirmationDialogContent( onCancelClicked = onCancelClicked, thirdButtonText = thirdButtonText, onThirdButtonClicked = onThirdButtonClicked, + destructiveSubmit = destructiveSubmit, icon = icon, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt index 2eb290dd4e..bdeceb2ac8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/AlertDialogContent.kt @@ -57,6 +57,7 @@ internal fun SimpleAlertDialogContent( title: String? = null, subtitle: @Composable (() -> Unit)? = null, submitText: String? = null, + destructiveSubmit: Boolean = false, onSubmitClicked: () -> Unit = {}, thirdButtonText: String? = null, onThirdButtonClicked: () -> Unit = {}, @@ -76,6 +77,7 @@ internal fun SimpleAlertDialogContent( title = title, subtitle = subtitle, submitText = submitText, + destructiveSubmit = destructiveSubmit, onSubmitClicked = onSubmitClicked, thirdButtonText = thirdButtonText, onThirdButtonClicked = onThirdButtonClicked, @@ -92,6 +94,7 @@ internal fun SimpleAlertDialogContent( title: String? = null, subtitle: @Composable (() -> Unit)? = null, submitText: String? = null, + destructiveSubmit: Boolean = false, onSubmitClicked: () -> Unit = {}, thirdButtonText: String? = null, onThirdButtonClicked: () -> Unit = {}, @@ -126,6 +129,7 @@ internal fun SimpleAlertDialogContent( enabled = enabled, size = ButtonSize.Medium, onClick = onSubmitClicked, + destructive = destructiveSubmit, ) } } @@ -345,8 +349,10 @@ private fun AlertDialogFlowRow( val arrangement = Arrangement.End val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } with(arrangement) { - arrange(mainAxisLayoutSize, childrenMainAxisSizes, - layoutDirection, mainAxisPositions) + arrange( + mainAxisLayoutSize, childrenMainAxisSizes, + layoutDirection, mainAxisPositions + ) } placeables.forEachIndexed { j, placeable -> placeable.place( @@ -385,22 +391,22 @@ internal object DialogContentDefaults { val containerColor: Color @Composable @ReadOnlyComposable - get()= ElementTheme.colors.bgCanvasDefault + get() = ElementTheme.colors.bgCanvasDefault val textContentColor: Color @Composable @ReadOnlyComposable - get()= ElementTheme.materialColors.onSurfaceVariant + get() = ElementTheme.materialColors.onSurfaceVariant val titleContentColor: Color @Composable @ReadOnlyComposable - get()= ElementTheme.materialColors.onSurface + get() = ElementTheme.materialColors.onSurface val iconContentColor: Color @Composable @ReadOnlyComposable - get()= ElementTheme.materialColors.primary + get() = ElementTheme.materialColors.primary } // Paddings for each of the dialog's parts. Taken from M3 source code. @@ -462,3 +468,21 @@ internal fun DialogWithOnlyMessageAndOkButtonPreview() { } } } + +@Preview(group = PreviewGroup.Dialogs, name = "Dialog with destructive button") +@Composable +@Suppress("MaxLineLength") +internal fun DialogWithDestructiveButtonPreview() { + ElementThemedPreview(showBackground = false) { + DialogPreview { + SimpleAlertDialogContent( + title = "Dialog Title", + content = "A dialog with a destructive action", + cancelText = "Cancel", + submitText = "Delete", + destructiveSubmit = true, + onCancelClicked = {}, + ) + } + } +} From 93e05e9d9f2c09d4c9df12889b0115f6ab0a9f89 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 3 Oct 2023 13:44:06 +0200 Subject: [PATCH 193/281] Preference: Add badge to the icon preference. --- .../components/preferences/PreferenceText.kt | 2 + .../preferences/components/PreferenceIcon.kt | 49 ++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) 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 d4bc627802..e0b8ea7926 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 @@ -57,6 +57,7 @@ fun PreferenceText( icon: ImageVector? = null, @DrawableRes iconResourceId: Int? = null, showIconAreaIfNoIcon: Boolean = false, + showIconBadge: Boolean = false, tintColor: Color? = null, onClick: () -> Unit = {}, ) { @@ -73,6 +74,7 @@ fun PreferenceText( PreferenceIcon( icon = icon, iconResourceId = iconResourceId, + showIconBadge = showIconBadge, enabled = enabled, isVisible = showIconAreaIfNoIcon, tintColor = tintColor ?: enabled.toSecondaryEnabledColor(), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt index d1c1281ee9..c29b46f3c3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt @@ -17,17 +17,20 @@ package io.element.android.libraries.designsystem.components.preferences.components import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.Icon @@ -38,20 +41,30 @@ fun PreferenceIcon( modifier: Modifier = Modifier, icon: ImageVector? = null, @DrawableRes iconResourceId: Int? = null, + showIconBadge: Boolean = false, tintColor: Color? = null, enabled: Boolean = true, isVisible: Boolean = true, ) { if (icon != null || iconResourceId != null) { - Icon( - imageVector = icon, - resourceId = iconResourceId, - contentDescription = "", - tint = tintColor ?: enabled.toSecondaryEnabledColor(), - modifier = modifier - .padding(end = 16.dp) - .size(24.dp), - ) + Box(modifier = modifier) { + Icon( + imageVector = icon, + resourceId = iconResourceId, + contentDescription = "", + tint = tintColor ?: enabled.toSecondaryEnabledColor(), + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp), + ) + if (showIconBadge) { + RedIndicatorAtom( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(end = 16.dp) + ) + } + } } else if (isVisible) { Spacer(modifier = modifier.width(40.dp)) } @@ -60,9 +73,19 @@ fun PreferenceIcon( @Preview(group = PreviewGroup.Preferences) @Composable internal fun PreferenceIconPreview(@PreviewParameter(ImageVectorProvider::class) content: ImageVector?) = - ElementThemedPreview { ContentToPreview(content) } + ElementThemedPreview { + PreferenceIcon( + icon = content, + showIconBadge = false, + ) + } +@Preview(group = PreviewGroup.Preferences) @Composable -private fun ContentToPreview(content: ImageVector?) { - PreferenceIcon(icon = content) -} +internal fun PreferenceIconWithBadgePreview(@PreviewParameter(ImageVectorProvider::class) content: ImageVector?) = + ElementThemedPreview { + PreferenceIcon( + icon = content, + showIconBadge = true, + ) + } From fa2a2e2f7f26460b8774255503ab6d0530c3866f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 24 Oct 2023 17:15:10 +0200 Subject: [PATCH 194/281] Design: extract DialogLikeBannerMolecule from RequestVerificationHeader --- .../components/RequestVerificationHeader.kt | 74 +++---------- .../atomic/molecules/HomeBannerMolecule.kt | 103 ++++++++++++++++++ 2 files changed, 116 insertions(+), 61 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt 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 index 991be692a0..d3ad6db206 100644 --- 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 @@ -16,31 +16,13 @@ package io.element.android.features.roomlist.impl.components -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import io.element.android.features.roomlist.impl.R -import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule import io.element.android.libraries.designsystem.preview.ElementPreview -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.Surface -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.theme.ElementTheme -import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.libraries.designsystem.preview.PreviewsDayNight @Composable internal fun RequestVerificationHeader( @@ -48,50 +30,20 @@ internal fun RequestVerificationHeader( onDismissClicked: () -> Unit, modifier: Modifier = Modifier, ) { - Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - Surface( - Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.surfaceVariant - ) { - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp) - ) { - Row { - Text( - stringResource(R.string.session_verification_banner_title), - modifier = Modifier.weight(1f), - style = ElementTheme.typography.fontBodyLgMedium, - color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Start, - ) - Icon( - modifier = Modifier.clickable(onClick = onDismissClicked), - resourceId = CommonDrawables.ic_compound_close, - contentDescription = stringResource(CommonStrings.action_close) - ) - } - Spacer(modifier = Modifier.height(4.dp)) - Text( - stringResource(R.string.session_verification_banner_message), - style = ElementTheme.typography.fontBodyMdRegular, - ) - Spacer(modifier = Modifier.height(12.dp)) - Button( - text = stringResource(CommonStrings.action_continue), - size = ButtonSize.Medium, - modifier = Modifier.fillMaxWidth(), - onClick = onVerifyClicked, - ) - } - } - } + 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 = {}) + RequestVerificationHeader( + onVerifyClicked = {}, + onDismissClicked = {}, + ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt new file mode 100644 index 0000000000..dd33cd1b79 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt @@ -0,0 +1,103 @@ +/* + * 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.atomic.molecules + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +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.ButtonSize +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun DialogLikeBannerMolecule( + title: String, + content: String, + onSubmitClicked: () -> Unit, + onDismissClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Surface( + Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.small, + color = MaterialTheme.colorScheme.surfaceVariant + ) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Row { + Text( + text = title, + modifier = Modifier.weight(1f), + style = ElementTheme.typography.fontBodyLgMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Start, + ) + Icon( + modifier = Modifier.clickable(onClick = onDismissClicked), + resourceId = CommonDrawables.ic_compound_close, + contentDescription = stringResource(CommonStrings.action_close) + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = content, + style = ElementTheme.typography.fontBodyMdRegular, + ) + Spacer(modifier = Modifier.height(12.dp)) + Button( + text = stringResource(CommonStrings.action_continue), + size = ButtonSize.Medium, + modifier = Modifier.fillMaxWidth(), + onClick = onSubmitClicked, + ) + } + } + } +} + +@PreviewsDayNight +@Composable +internal fun DialogLikeBannerMoleculePreview() = ElementPreview { + DialogLikeBannerMolecule( + title = "Title", + content = "Content", + onSubmitClicked = {}, + onDismissClicked = {} + ) +} From 96dd7bbd8d52f4da1be28c75e796f69b46050662 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Oct 2023 17:09:57 +0200 Subject: [PATCH 195/281] Use DialogLikeBannerMolecule in NotificationSettingsView. --- .../notifications/NotificationSettingsView.kt | 63 ++++--------------- .../atomic/molecules/HomeBannerMolecule.kt | 14 +++-- 2 files changed, 19 insertions(+), 58 deletions(-) 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 223107226e..a1d248c1fd 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 @@ -16,41 +16,27 @@ package io.element.android.features.preferences.impl.notifications -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.atomic.molecules.DialogLikeBannerMolecule import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceText -import io.element.android.libraries.designsystem.components.preferences.PreferencePage -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview -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.Surface -import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings /** @@ -197,41 +183,14 @@ private fun InvalidNotificationSettingsView( onDismissError: () -> Unit, modifier: Modifier = Modifier ) { - Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - Surface( - Modifier.fillMaxWidth(), - shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.surfaceVariant - ) { - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp) - ) { - Row { - Text( - stringResource(CommonStrings.screen_notification_settings_configuration_mismatch), - modifier = Modifier.weight(1f), - style = ElementTheme.typography.fontBodyLgMedium, - color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Start, - ) - } - Spacer(modifier = Modifier.height(4.dp)) - Text( - stringResource(CommonStrings.screen_notification_settings_configuration_mismatch_description), - style = ElementTheme.typography.fontBodyMdRegular, - ) - Spacer(modifier = Modifier.height(12.dp)) - Button( - text = stringResource(CommonStrings.action_continue), - size = ButtonSize.Medium, - modifier = Modifier.fillMaxWidth(), - onClick = onContinueClicked, - ) - } - } - } + DialogLikeBannerMolecule( + modifier = modifier, + title = stringResource(CommonStrings.screen_notification_settings_configuration_mismatch), + content = stringResource(CommonStrings.screen_notification_settings_configuration_mismatch_description), + onSubmitClicked = onContinueClicked, + onDismissClicked = null, + ) + if (showError) { ErrorDialog( title = stringResource(id = CommonStrings.dialog_title_error), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt index dd33cd1b79..1be5f12b0f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt @@ -46,7 +46,7 @@ fun DialogLikeBannerMolecule( title: String, content: String, onSubmitClicked: () -> Unit, - onDismissClicked: () -> Unit, + onDismissClicked: (() -> Unit)?, modifier: Modifier = Modifier, ) { Box(modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { @@ -68,11 +68,13 @@ fun DialogLikeBannerMolecule( color = MaterialTheme.colorScheme.primary, textAlign = TextAlign.Start, ) - Icon( - modifier = Modifier.clickable(onClick = onDismissClicked), - resourceId = CommonDrawables.ic_compound_close, - contentDescription = stringResource(CommonStrings.action_close) - ) + if (onDismissClicked != null) { + Icon( + modifier = Modifier.clickable(onClick = onDismissClicked), + resourceId = CommonDrawables.ic_compound_close, + contentDescription = stringResource(CommonStrings.action_close) + ) + } } Spacer(modifier = Modifier.height(4.dp)) Text( From 4a7dc3fedab6419d37d962ae8f2bc6208e329d75 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 26 Oct 2023 15:25:07 +0000 Subject: [PATCH 196/281] Update screenshots --- ...c.atoms_null_RedIndicatorAtom-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ...c.atoms_null_RedIndicatorAtom-N_1_null,NEXUS_5,1.0,en].png | 3 +++ ...null_DialogLikeBannerMolecule-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ...null_DialogLikeBannerMolecule-N_1_null,NEXUS_5,1.0,en].png | 3 +++ ...ences_PreferenceIconWithBadge_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ences_PreferenceIconWithBadge_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ..._null_Buttons_FilledButtonLarge_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...null_Buttons_FilledButtonMedium_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ull_Buttons_OutlinedButtonLarge_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ll_Buttons_OutlinedButtonMedium_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ts_null_Buttons_TextButtonLarge_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...s_null_Buttons_TextButtonMedium_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png | 3 +++ 13 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_RedIndicatorAtom-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_RedIndicatorAtom-N_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_DialogLikeBannerMolecule-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_DialogLikeBannerMolecule-N_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences.components_null_Preferences_PreferenceIconWithBadge_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences.components_null_Preferences_PreferenceIconWithBadge_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_RedIndicatorAtom-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_RedIndicatorAtom-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7593c75aaf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_RedIndicatorAtom-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f54912dfb9235aa915025d496f280de362286997f746481da41deb02a1ec67e3 +size 4768 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_RedIndicatorAtom-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_RedIndicatorAtom-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f1ff44eecf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.atoms_null_RedIndicatorAtom-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae078e1bb55967e9e583a588b4fb410eb0417d944b20911158b24f3824ee6dca +size 4752 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_DialogLikeBannerMolecule-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_DialogLikeBannerMolecule-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9315297856 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_DialogLikeBannerMolecule-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45736b242f9bf1a50fb14af9b340633f0fdc348ed7a846c4c57c3a56a4b2a48c +size 11429 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_DialogLikeBannerMolecule-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_DialogLikeBannerMolecule-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7defe86721 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.atomic.molecules_null_DialogLikeBannerMolecule-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f9989dadb7c68cf705683997689f48595be9283eb939d934819399ec3c6805f +size 11209 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences.components_null_Preferences_PreferenceIconWithBadge_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences.components_null_Preferences_PreferenceIconWithBadge_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e62a06473c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences.components_null_Preferences_PreferenceIconWithBadge_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6d2243cb6bedd4b7a2f1c8c9b5aeac8356642271061c1439e591ce07c55c822 +size 6390 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences.components_null_Preferences_PreferenceIconWithBadge_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences.components_null_Preferences_PreferenceIconWithBadge_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3c228b96a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences.components_null_Preferences_PreferenceIconWithBadge_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2d302d58c1faec3c3f0aa3286977b2698cd642c1e6dbcb9632510c6a4a922eb +size 4501 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_FilledButtonLarge_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_FilledButtonLarge_0_null,NEXUS_5,1.0,en].png index a82b9c0a1c..1ef44d7b1d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_FilledButtonLarge_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_FilledButtonLarge_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0f2dc7bfcf59bf26362775625ddc9dedf53f400057997560628614136e2880b -size 44144 +oid sha256:222795141c044e5bd7929cb9b894535124a176fd88347eea83037116f4ba6e8b +size 68192 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_FilledButtonMedium_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_FilledButtonMedium_0_null,NEXUS_5,1.0,en].png index 322c44b61f..4d5c49c7a8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_FilledButtonMedium_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_FilledButtonMedium_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e240c572aab6db77995c93787a7b0aaba76060861c45934eb87d4ba48c5675c -size 42789 +oid sha256:b2d816dac6d468ede766719ec854cd575c89f454346ddd6ef8048c836bd04c14 +size 66223 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_OutlinedButtonLarge_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_OutlinedButtonLarge_0_null,NEXUS_5,1.0,en].png index 47ca9e7c31..d34a8c1d62 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_OutlinedButtonLarge_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_OutlinedButtonLarge_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54497bfab92c6ed850f753df61a31366ca68d41c7d90e322e7cc7eceb6dd4277 -size 48139 +oid sha256:a8d423b71168b3bd5dab8da01376fa3f114865aa0d4eb48d41fccfb4640367e3 +size 77867 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_OutlinedButtonMedium_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_OutlinedButtonMedium_0_null,NEXUS_5,1.0,en].png index 916f75d13e..be25d4cf37 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_OutlinedButtonMedium_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_OutlinedButtonMedium_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7d3f2e50b395f86ed7c78146d5a428b099c672d763c278c1b2899d915abf717 -size 47615 +oid sha256:5381c6f070f2c81019ce39af5902f6f2c683c7ce4db4636b3b3259fdca270cdf +size 75884 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_TextButtonLarge_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_TextButtonLarge_0_null,NEXUS_5,1.0,en].png index 996018bf70..2fa4c52ecb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_TextButtonLarge_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_TextButtonLarge_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:876eba4a9f8214aa2d33dad84ccbbb95994c615f25f158cb6596fd4512241bd9 -size 31582 +oid sha256:093a5bd68ebca0a9eaf8cac3cf748514cc0f3165c1079016d54d40762c7c0287 +size 46434 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_TextButtonMedium_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_TextButtonMedium_0_null,NEXUS_5,1.0,en].png index 021f9c9358..89b4df8c93 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_TextButtonMedium_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Buttons_TextButtonMedium_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dd7259e0b14b9ad5e9a6aed46383f91822ee11f1e77b0f2cdc5d2e0b38f7abd -size 29702 +oid sha256:dfa423f2008049e54046449c19615eb66f28e7e66645eff76e80e33062b5c59a +size 43655 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8bc5d4dfb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3079a82a70fc20feac8c694a8571eac5dac9e2dea800bd7ca01628fe7c90d29 +size 33019 From ea9c7f5b103ca6f6675203af4973e2a14e5a6a5d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Oct 2023 17:21:19 +0200 Subject: [PATCH 197/281] Rename file. --- .../{HomeBannerMolecule.kt => DialogLikeBannerMolecule.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/{HomeBannerMolecule.kt => DialogLikeBannerMolecule.kt} (100%) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt similarity index 100% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/HomeBannerMolecule.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/DialogLikeBannerMolecule.kt From bf5047647aad0d12f9a30ff640dd27eddd4c9205 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Oct 2023 17:49:13 +0200 Subject: [PATCH 198/281] Fix warning: Composable functions should only be emitting content into the composition from one source at their top level. --- .../android/libraries/designsystem/theme/components/Button.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt index b3bc9a01bd..5ce528c814 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Button.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -379,7 +380,7 @@ private fun ButtonCombinationPreview( } @Composable -private fun ButtonMatrixPreview( +private fun ColumnScope.ButtonMatrixPreview( style: ButtonStyle, size: ButtonSize, destructive: Boolean, From 3a15b92eb6aa8dc67ee7014bfecc24b892443232 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 26 Oct 2023 18:02:34 +0200 Subject: [PATCH 199/281] SecureBackup: update matrix sdk module. --- .../impl/unlock/PinUnlockPresenter.kt | 2 +- .../impl/DefaultLogoutPreferencePresenter.kt | 2 +- .../libraries/matrix/api/MatrixClient.kt | 5 +- .../api/encryption/BackupUploadState.kt | 35 +++++ .../api/encryption/EnableRecoveryProgress.kt | 25 ++++ .../api/encryption/EncryptionService.kt | 52 +++++++ .../matrix/api/encryption/RecoveryState.kt | 24 ++++ .../libraries/matrix/impl/RustMatrixClient.kt | 27 +++- .../matrix/impl/di/SessionMatrixModule.kt | 6 + .../encryption/BackupUploadStateMapper.kt | 41 ++++++ .../EnableRecoveryProgressMapper.kt | 36 +++++ .../impl/encryption/RecoveryStateMapper.kt | 31 ++++ .../impl/encryption/RustEncryptionService.kt | 133 ++++++++++++++++++ .../RustSessionVerificationService.kt | 3 +- .../libraries/matrix/test/FakeMatrixClient.kt | 10 +- .../android/libraries/matrix/test/TestData.kt | 2 + .../test/encryption/FakeEncryptionService.kt | 95 +++++++++++++ 17 files changed, 518 insertions(+), 11 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index b26f967b4c..ed8ee1933d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -142,7 +142,7 @@ class PinUnlockPresenter @Inject constructor( private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { - matrixClient.logout() + matrixClient.logout(ignoreSdkError = true) }.runCatchingUpdatingState(signOutAction) } } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt index 2fece4449b..49d0606633 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt @@ -58,7 +58,7 @@ class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixCli private fun CoroutineScope.logout(logoutAction: MutableState>) = launch { suspend { - matrixClient.logout() + matrixClient.logout(false /* TODO */) }.runCatchingUpdatingState(logoutAction) } } 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 fc215e4780..f9ac0d84a5 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 @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters +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.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -55,6 +56,7 @@ interface MatrixClient : Closeable { fun pushersService(): PushersService fun notificationService(): NotificationService fun notificationSettingsService(): NotificationSettingsService + fun encryptionService(): EncryptionService suspend fun getCacheSize(): Long /** @@ -66,8 +68,9 @@ interface MatrixClient : Closeable { * Logout the user. * Returns an optional URL. When the URL is there, it should be presented to the user after logout for * Relying Party (RP) initiated logout on their account page. + * @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway. */ - suspend fun logout(): String? + suspend fun logout(ignoreSdkError: Boolean): String? suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.kt new file mode 100644 index 0000000000..4424db4684 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/BackupUploadState.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.matrix.api.encryption + +sealed interface BackupUploadState { + data object Unknown : BackupUploadState + + data class CheckingIfUploadNeeded( + val backedUpCount: Int, + val totalCount: Int, + ) : BackupUploadState + + data object Waiting : BackupUploadState + + data class Uploading( + val backedUpCount: Int, + val totalCount: Int, + ) : BackupUploadState + + data object Done : BackupUploadState +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.kt new file mode 100644 index 0000000000..1ee85a1cc1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EnableRecoveryProgress.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.matrix.api.encryption + +sealed interface EnableRecoveryProgress { + data object Unknown : EnableRecoveryProgress + data object CreatingRecoveryKey : EnableRecoveryProgress + data object CreatingBackup : EnableRecoveryProgress + data class BackingUp(val backedUpCount: Int, val totalCount: Int) : EnableRecoveryProgress + data class Done(val recoveryKey: String) : EnableRecoveryProgress +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt new file mode 100644 index 0000000000..b11cc3f9ab --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -0,0 +1,52 @@ +/* + * 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.api.encryption + +import kotlinx.coroutines.flow.StateFlow + +interface EncryptionService { + val backupStateStateFlow: StateFlow + val recoveryStateStateFlow: StateFlow + val backupUploadStateStateFlow: StateFlow + val enableRecoveryProgressStateFlow: StateFlow + + suspend fun enableBackups(): Result + + suspend fun isLastDevice(): Result + + /** + * Enable recovery. Observe enableProgressStateFlow to get progress and recovery key. + */ + suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result + + /** + * Change the recovery and return the new recovery key. + */ + suspend fun resetRecoveryKey(): Result + + suspend fun disableRecovery(): Result + + /** + * Note: accept bot recoveryKey and passphrase. + */ + suspend fun fixRecoveryIssues(recoveryKey: String): Result + + /** + * Observe [backupUploadStateStateFlow] to get progress. + */ + suspend fun waitForBackupUploadSteadyState(): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt new file mode 100644 index 0000000000..c033332d8f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/RecoveryState.kt @@ -0,0 +1,24 @@ +/* + * 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.api.encryption + +enum class RecoveryState { + UNKNOWN, + ENABLED, + DISABLED, + INCOMPLETE, +} 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 67bbcb835c..c339bca26c 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 @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility +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.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -41,6 +42,7 @@ 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 import io.element.android.libraries.matrix.impl.core.toProgressWatcher +import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.notification.RustNotificationService @@ -117,6 +119,7 @@ class RustMatrixClient constructor( private val notificationService = RustNotificationService(sessionId, notificationClient, dispatchers, clock) private val notificationSettingsService = RustNotificationSettingsService(notificationSettings, dispatchers) private val roomSyncSubscriber = RoomSyncSubscriber(innerRoomListService, dispatchers) + private val encryptionService = RustEncryptionService(client, dispatchers).apply { start() } private val isLoggingOut = AtomicBoolean(false) @@ -136,7 +139,7 @@ class RustMatrixClient constructor( ) sessionStore.updateData(newData) } - doLogout(doRequest = false, removeSession = false) + doLogout(doRequest = false, removeSession = false, ignoreSdkError = false) } } else { Timber.v("didReceiveAuthError -> already cleaning up") @@ -319,6 +322,8 @@ class RustMatrixClient constructor( override fun notificationService(): NotificationService = notificationService + override fun encryptionService(): EncryptionService = encryptionService + override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService override fun close() { @@ -331,6 +336,7 @@ class RustMatrixClient constructor( innerRoomListService.destroy() notificationClient.destroy() notificationProcessSetup.destroy() + encryptionService.destroy() client.destroy() } @@ -344,16 +350,29 @@ class RustMatrixClient constructor( baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false) } - override suspend fun logout(): String? = doLogout(doRequest = true, removeSession = true) + override suspend fun logout(ignoreSdkError: Boolean): String? = doLogout( + doRequest = true, + removeSession = true, + ignoreSdkError = ignoreSdkError, + ) - private suspend fun doLogout(doRequest: Boolean, removeSession: Boolean): String? { + private suspend fun doLogout( + doRequest: Boolean, + removeSession: Boolean, + ignoreSdkError: Boolean, + ): String? { var result: String? = null withContext(sessionDispatcher) { if (doRequest) { try { result = client.logout() } catch (failure: Throwable) { - Timber.e(failure, "Fail to call logout on HS. Still delete local files.") + if (ignoreSdkError) { + Timber.e(failure, "Fail to call logout on HS. Still delete local files.") + } else { + Timber.e(failure, "Fail to call logout on HS.") + throw failure + } } } 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 1159f668e6..71f01f0aac 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 @@ -21,6 +21,7 @@ import dagger.Module import dagger.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient +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 @@ -50,6 +51,11 @@ object SessionMatrixModule { return matrixClient.roomListService } + @Provides + fun providesEncryptionService(matrixClient: MatrixClient): EncryptionService { + return matrixClient.encryptionService() + } + @Provides fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader { return matrixClient.mediaLoader diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt new file mode 100644 index 0000000000..71bc8a081a --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/BackupUploadStateMapper.kt @@ -0,0 +1,41 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState + +class BackupUploadStateMapper { + fun map(rustEnableProgress: RustBackupUploadState): BackupUploadState { + return when (rustEnableProgress) { + is RustBackupUploadState.CheckingIfUploadNeeded -> + BackupUploadState.CheckingIfUploadNeeded( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + RustBackupUploadState.Done -> + BackupUploadState.Done + is RustBackupUploadState.Uploading -> + BackupUploadState.Uploading( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + RustBackupUploadState.Waiting -> + BackupUploadState.Waiting + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt new file mode 100644 index 0000000000..a50a68267d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/EnableRecoveryProgressMapper.kt @@ -0,0 +1,36 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress + +class EnableRecoveryProgressMapper { + fun map(rustEnableProgress: RustEnableRecoveryProgress): EnableRecoveryProgress { + return when (rustEnableProgress) { + is RustEnableRecoveryProgress.CreatingRecoveryKey -> EnableRecoveryProgress.CreatingRecoveryKey + is RustEnableRecoveryProgress.CreatingBackup -> EnableRecoveryProgress.CreatingBackup + is RustEnableRecoveryProgress.BackingUp -> EnableRecoveryProgress.BackingUp( + backedUpCount = rustEnableProgress.backedUpCount.toInt(), + totalCount = rustEnableProgress.totalCount.toInt(), + ) + is RustEnableRecoveryProgress.Done -> EnableRecoveryProgress.Done( + recoveryKey = rustEnableProgress.recoveryKey + ) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt new file mode 100644 index 0000000000..8d9050f22d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RecoveryStateMapper.kt @@ -0,0 +1,31 @@ +/* + * 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.encryption + +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState + +class RecoveryStateMapper { + fun map(state: RustRecoveryState): RecoveryState { + return when (state) { + RustRecoveryState.UNKNOWN -> RecoveryState.UNKNOWN + RustRecoveryState.ENABLED -> RecoveryState.ENABLED + RustRecoveryState.DISABLED -> RecoveryState.DISABLED + RustRecoveryState.INCOMPLETE -> RecoveryState.INCOMPLETE + } + } +} 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 new file mode 100644 index 0000000000..9b1e3909d7 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -0,0 +1,133 @@ +/* + * 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.encryption + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.BackupStateListener +import org.matrix.rustcomponents.sdk.BackupSteadyStateListener +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener +import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.RecoveryStateListener +import org.matrix.rustcomponents.sdk.BackupState as RustBackupState +import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState +import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress +import org.matrix.rustcomponents.sdk.RecoveryState as RustRecoveryState + +internal class RustEncryptionService( + client: Client, + private val dispatchers: CoroutineDispatchers, +) : EncryptionService { + + private val service: Encryption = client.encryption() + + private val backupStateMapper = BackupStateMapper() + private val recoveryStateMapper = RecoveryStateMapper() + private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() + private val backupUploadStateMapper = BackupUploadStateMapper() + + override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(service.backupState().let(backupStateMapper::map)) + override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map)) + override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) + override val backupUploadStateStateFlow: MutableStateFlow = MutableStateFlow(BackupUploadState.Unknown) + + fun start() { + service.backupStateListener(object : BackupStateListener { + override fun onUpdate(status: RustBackupState) { + backupStateStateFlow.value = backupStateMapper.map(status) + } + }) + + service.recoveryStateListener(object : RecoveryStateListener { + override fun onUpdate(status: RustRecoveryState) { + recoveryStateStateFlow.value = recoveryStateMapper.map(status) + } + }) + } + + fun destroy() { + // No way to remove the listeners... + service.destroy() + } + + override suspend fun enableBackups(): Result = withContext(dispatchers.io) { + runCatching { + service.enableBackups() + } + } + + override suspend fun enableRecovery( + waitForBackupsToUpload: Boolean, + ): Result = withContext(dispatchers.io) { + runCatching { + service.enableRecovery( + waitForBackupsToUpload = waitForBackupsToUpload, + progressListener = object : EnableRecoveryProgressListener { + override fun onUpdate(status: RustEnableRecoveryProgress) { + enableRecoveryProgressStateFlow.value = enableRecoveryProgressMapper.map(status) + } + } + ) + // enableRecovery returns the encryption key, but we read it from the state flow + .let { } + } + } + + override suspend fun waitForBackupUploadSteadyState( + ): Result = withContext(dispatchers.io) { + runCatching { + service.waitForBackupUploadSteadyState( + progressListener = object : BackupSteadyStateListener { + override fun onUpdate(status: RustBackupUploadState) { + backupUploadStateStateFlow.value = backupUploadStateMapper.map(status) + } + } + ) + } + } + + override suspend fun disableRecovery(): Result = withContext(dispatchers.io) { + runCatching { + service.disableRecovery() + } + } + + override suspend fun isLastDevice(): Result = withContext(dispatchers.io) { + runCatching { + service.isLastDevice() + } + } + + override suspend fun resetRecoveryKey(): Result = withContext(dispatchers.io) { + runCatching { + service.resetRecoveryKey() + } + } + + override suspend fun fixRecoveryIssues(recoveryKey: String): Result = withContext(dispatchers.io) { + runCatching { + service.fixRecoveryIssues(recoveryKey) + } + } +} 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 6616539e95..a4dfadfa5b 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 @@ -33,9 +33,8 @@ import org.matrix.rustcomponents.sdk.SessionVerificationController import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate import org.matrix.rustcomponents.sdk.SessionVerificationControllerInterface import org.matrix.rustcomponents.sdk.SessionVerificationEmoji -import javax.inject.Inject -class RustSessionVerificationService @Inject constructor( +class RustSessionVerificationService( private val syncService: RustSyncService, private val sessionCoroutineScope: CoroutineScope, ) : SessionVerificationService, SessionVerificationControllerDelegate { 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 2332a37fdc..e8228e806e 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 @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters +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.notification.NotificationService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService @@ -33,6 +34,7 @@ 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 import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService 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 @@ -55,6 +57,7 @@ class FakeMatrixClient( private val notificationService: FakeNotificationService = FakeNotificationService(), private val notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), private val syncService: FakeSyncService = FakeSyncService(), + private val encryptionService: FakeEncryptionService = FakeEncryptionService(), private val accountManagementUrlString: Result = Result.success(null), ) : MatrixClient { @@ -124,9 +127,11 @@ class FakeMatrixClient( override suspend fun clearCache() { } - override suspend fun logout(): String? { + override suspend fun logout(ignoreSdkError: Boolean): String? { delay(100) - logoutFailure?.let { throw it } + if (ignoreSdkError.not()) { + logoutFailure?.let { throw it } + } return null } @@ -173,6 +178,7 @@ class FakeMatrixClient( override fun notificationService(): NotificationService = notificationService override fun notificationSettingsService(): NotificationSettingsService = notificationSettingsService + override fun encryptionService(): EncryptionService = encryptionService override fun roomMembershipObserver(): RoomMembershipObserver { return RoomMembershipObserver() diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 8e71447716..ca55e6d514 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -72,3 +72,5 @@ const val A_FAILURE_REASON = "There has been a failure" val A_THROWABLE = Throwable(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON) + +const val A_RECOVERY_KEY = "1234 5678" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt new file mode 100644 index 0000000000..548b2f9165 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -0,0 +1,95 @@ +/* + * 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.test.encryption + +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeEncryptionService : EncryptionService { + private var disableRecoveryFailure: Exception? = null + override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) + override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) + override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) + override val backupUploadStateStateFlow: MutableStateFlow = MutableStateFlow(BackupUploadState.Unknown) + + private var fixRecoveryIssuesFailure: Exception? = null + + override suspend fun enableBackups(): Result = simulateLongTask { + return Result.success(Unit) + } + + fun givenDisableRecoveryFailure(exception: Exception) { + disableRecoveryFailure = exception + } + + fun givenFixRecoveryIssuesFailure(exception: Exception?) { + fixRecoveryIssuesFailure = exception + } + + override suspend fun disableRecovery(): Result = simulateLongTask { + disableRecoveryFailure?.let { return Result.failure(it) } + return Result.success(Unit) + } + + override suspend fun fixRecoveryIssues(recoveryKey: String): Result = simulateLongTask { + fixRecoveryIssuesFailure?.let { return Result.failure(it) } + return Result.success(Unit) + } + + private var isLastDevice = false + + fun givenIsLastDevice(isLastDevice: Boolean) { + this.isLastDevice = isLastDevice + } + + override suspend fun isLastDevice(): Result { + return Result.success(isLastDevice) + } + + override suspend fun resetRecoveryKey(): Result = simulateLongTask { + return Result.success(fakeRecoveryKey) + } + + override suspend fun enableRecovery(waitForBackupsToUpload: Boolean): Result = simulateLongTask { + return Result.success(Unit) + } + + override suspend fun waitForBackupUploadSteadyState(): Result { + return Result.success(Unit) + } + + suspend fun emitBackupUploadState(state: BackupUploadState) { + backupUploadStateStateFlow.emit(state) + } + + suspend fun emitBackupState(state: BackupState) { + backupStateStateFlow.emit(state) + } + + suspend fun emitEnableRecoveryProgress(state: EnableRecoveryProgress) { + enableRecoveryProgressStateFlow.emit(state) + } + + companion object { + const val fakeRecoveryKey = "fake" + } +} From 0457e5915c8029816fad92b01480299de7be2433 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 26 Oct 2023 18:58:00 +0200 Subject: [PATCH 200/281] Convert mx waveform to floats as early as possible in the chain (#1652) This way we're sure that internally we always deal with [0;1] float samples. the [0;1024] int range is used only at the rust sdk boundary. --- .../components/event/TimelineItemVoiceView.kt | 3 +- .../TimelineItemContentMessageFactory.kt | 3 +- .../model/event/TimelineItemVoiceContent.kt | 2 +- .../event/TimelineItemVoiceContentProvider.kt | 6 +-- .../impl/voicemessages/WaveformUtils.kt | 23 +++++++++ .../designsystem/components/media/Waveform.kt | 50 ------------------- .../components/media/WaveformPlaybackView.kt | 38 +++++++++++--- ...iceView-D-40_40_null_5,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-40_40_null_6,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-40_40_null_7,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-40_40_null_8,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-40_40_null_9,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_5,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_6,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_7,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_8,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-40_41_null_9,NEXUS_5,1.0,en].png | 4 +- ...ewUnified-D-41_41_null,NEXUS_5,1.0,en].png | 4 +- ...ewUnified-N-41_42_null,NEXUS_5,1.0,en].png | 4 +- ...mPlaybackView-D_0_null,NEXUS_5,1.0,en].png | 4 +- ...mPlaybackView-N_1_null,NEXUS_5,1.0,en].png | 4 +- 21 files changed, 88 insertions(+), 93 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index 34bdc75ca2..e02773131d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -44,7 +44,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider -import io.element.android.libraries.designsystem.components.media.Waveform import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -88,7 +87,7 @@ fun TimelineItemVoiceView( WaveformPlaybackView( showCursor = state.button == VoiceMessageState.Button.Pause, playbackProgress = state.progress, - waveform = Waveform(data = content.waveform), + waveform = content.waveform, modifier = Modifier .height(34.dp) .weight(1f), 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 4f78c38393..3262653134 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 @@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor import io.element.android.features.messages.impl.timeline.util.toHtmlDocument +import io.element.android.features.messages.impl.voicemessages.fromMSC3246range import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -117,7 +118,7 @@ class TimelineItemContentMessageFactory @Inject constructor( mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, - waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(), + waveform = messageType.details?.waveform?.fromMSC3246range()?.toImmutableList() ?: persistentListOf(), ) else -> TimelineItemAudioContent( body = messageType.body, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt index ec4647b7bd..0d3bb903de 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt @@ -27,7 +27,7 @@ data class TimelineItemVoiceContent( val duration: Duration, val mediaSource: MediaSource, val mimeType: String, - val waveform: ImmutableList, + val waveform: ImmutableList, ) : TimelineItemEventContent { override val type: String = "TimelineItemAudioContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt index 8c9ac1f21c..830255f06e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt @@ -32,11 +32,11 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0), + waveform: List = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), ) = TimelineItemVoiceContent( eventId = eventId?.let { EventId(it) }, body = body, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt new file mode 100644 index 0000000000..354e2eba7d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt @@ -0,0 +1,23 @@ +/* + * 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.voicemessages + +/** + * Resizes the given [0;1024] int list as per unstable MSC3246 spec + * to a [0;1] range float list to be used for waveform rendering. + */ +fun List.fromMSC3246range(): List = map { it / 1024f } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt deleted file mode 100644 index 4e86c87d18..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/Waveform.kt +++ /dev/null @@ -1,50 +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.components.media - -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlin.math.roundToInt - -data class Waveform ( - val data: ImmutableList -) { - companion object { - private val dataRange = 0..1024 - } - - fun normalisedData(maxSamplesCount: Int): ImmutableList { - if(maxSamplesCount <= 0) { - return persistentListOf() - } - - // Filter the data to keep only the expected number of samples - val result = if (data.size > maxSamplesCount) { - (0.. - val targetIndex = (index.toDouble() * (data.count().toDouble() / maxSamplesCount.toDouble())).roundToInt() - data[targetIndex] - } - } else { - data - } - - // Normalize the sample in the allowed range - return result.map { it.toFloat() / dataRange.last.toFloat() }.toPersistentList() - } -} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index d9d15c1c3b..b5d472ef69 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -46,22 +46,25 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlin.math.max +import kotlin.math.roundToInt private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F + @OptIn(ExperimentalComposeUiApi::class) @Composable fun WaveformPlaybackView( playbackProgress: Float, showCursor: Boolean, - waveform: Waveform, + waveform: ImmutableList, modifier: Modifier = Modifier, onSeek: (progress: Float) -> Unit = {}, brush: Brush = SolidColor(ElementTheme.colors.iconQuaternary), - progressBrush: Brush = SolidColor(ElementTheme.colors.iconSecondary), - cursorBrush: Brush = SolidColor(ElementTheme.colors.iconAccentTertiary), + progressBrush: Brush = SolidColor(ElementTheme.colors.iconSecondary), + cursorBrush: Brush = SolidColor(ElementTheme.colors.iconAccentTertiary), lineWidth: Dp = 2.dp, linePadding: Dp = 2.dp, minimumGraphAmplitude: Float = 2F, @@ -145,7 +148,7 @@ fun WaveformPlaybackView( ), blendMode = BlendMode.SrcAtop ) - if(showCursor || seekProgress.value != null) { + if (showCursor || seekProgress.value != null) { drawRoundRect( brush = cursorBrush, topLeft = Offset( @@ -166,24 +169,43 @@ fun WaveformPlaybackView( @PreviewsDayNight @Composable internal fun WaveformPlaybackViewPreview() = ElementPreview { - Column{ + Column { WaveformPlaybackView( modifier = Modifier.height(34.dp), showCursor = false, playbackProgress = 0.5f, - waveform = Waveform(persistentListOf()), + waveform = persistentListOf(), ) WaveformPlaybackView( modifier = Modifier.height(34.dp), showCursor = false, playbackProgress = 0.5f, - waveform = Waveform(persistentListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)), + waveform = persistentListOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), ) WaveformPlaybackView( modifier = Modifier.height(34.dp), showCursor = true, playbackProgress = 0.5f, - waveform = Waveform(List(1024) { it }.toPersistentList()), + waveform = List(1024) { it / 1024f }.toPersistentList(), ) } } + +private fun ImmutableList.normalisedData(maxSamplesCount: Int): ImmutableList { + if (maxSamplesCount <= 0) { + return persistentListOf() + } + + // Filter the data to keep only the expected number of samples + val result = if (this.size > maxSamplesCount) { + (0.. + val targetIndex = (index.toDouble() * (this.count().toDouble() / maxSamplesCount.toDouble())).roundToInt() + this[targetIndex] + } + } else { + this + } + + return result.toPersistentList() +} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png index e2de1a2da9..2ec7b91240 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18378d89af53ac54931d3feb3129ff90e0e45c707144322de3d8740fd3ce3645 -size 6436 +oid sha256:e9ea30725d03749028f907e341ee09bb98a67cc6bda76757f7b3d266fed3722f +size 7177 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png index 663dc09151..f189640668 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d95ae8cc103ad50397c804ba2fe16c3b4fb380456f2b3be6f99ec62877b36758 -size 6429 +oid sha256:52ae85fdd9eeb0e3d81336c38147d8251079e2ae61364ca7f7cd573676dc2c57 +size 7085 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png index e6c7409c2e..7f1bd43732 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10058823be0d5f0a23e1712d4523188729dd7579005a48a63e8e553baea105c0 -size 5936 +oid sha256:da00e2729bcd8ac55de08a2ff2e101e6746a64a5fdc07c8ddbb8297543dc493f +size 6663 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png index ad49bcfa8d..c18ddb3953 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12eacc1babdc55ec2faed630889157ea0c9e6dca8845ecc93bc72bd645f23a9e -size 6481 +oid sha256:d44ed3c1ff4607171e21c580f337ca0a5664992fb90d2072b511af8e72ac8353 +size 7070 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png index 80ae6763ba..1a1c09685c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e4ee91fc8599e4c3e1d093587a9688c36df971a94bb832e6662c86572fb08c7 -size 6436 +oid sha256:7eb5e7f99047da383a6bc5a45eb76bd902c21fcede196626fc4f6f5507097d51 +size 7333 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png index 1078abcc5a..aa9de6ea89 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2649d3974b096f17a6e80d728b08409c0f1b28ed649f33c7798f6223f55ca29 -size 6343 +oid sha256:d4a2824fdc49adb0e3fc155e7da319414544a87dc083bdece78e85ba72669d78 +size 7155 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png index b2a3aef0c5..4e5b8e146d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7841691e0bbcfa0655304ac6a7c0ea24353aae9bfc264dc7978c2a4526781ee3 -size 6357 +oid sha256:3817e348ba2775528e3805ad6f5eed044f82b241f73692180c0638c95fb79fa2 +size 7076 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png index a0e382a78c..32b8d33a13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1a1ad6a3c02f24a8b811197f6999d93db57d3c1e9b867638c88f4759e9f1b87 -size 5926 +oid sha256:fd21f1c5ad2616b698a386d78d5efe0ab40aa1959382ed2a568c10441d093a8f +size 6692 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png index 5ec0a0c07f..dbee328561 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0adbfdd9131913c11d7e4777b4bdfbda84b3a9c6f1784781698d9208023e9c39 -size 6400 +oid sha256:07f52161378dfab92ffb76b860680390ccb5dfcc05347c5fd99504c806dd3210 +size 6930 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png index 510775c570..d997afeeb6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf18143a576ef0a136ca6e07c51521f5d41b6b5f381d168ff311cdf099752550 -size 6349 +oid sha256:4adce5249b20bdc9ed6f4d5f066140e8c9b0f62d04ebf953704a106a6ca2ff53 +size 7178 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png index fad12e2edc..6b0bdb01df 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:216e36042ad1ef11331d8610ab32383112bde38e314ae0d8c42d23931c7874a2 -size 43748 +oid sha256:5c52825ef36b00ee13496c23db981c226a3f71c0d1755508835b4ab3a140cff6 +size 46402 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png index 8c7ece47b1..d76cf9b40d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2542c95c067d4d65e76ea606801dc3dbfc196d8128a8547ed3e1bd93b84d4ab9 -size 42513 +oid sha256:ceb0cfa1b7ac95de658e339d29b4e18d8e4673d1a269db3d46bc6fd97381e9c2 +size 45460 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png index fb4d22eff4..e762d06dd7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efc60fcfb39718bfce62f16e82a0c85249b71bf850b2d996bb1234a882261bc9 -size 10021 +oid sha256:dbe3214d0b8c497f563957c9df870aa7b6cbf981e174a7f9481cc1555af50533 +size 10247 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png index dfc884aa45..b4f6fc2a60 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.media_null_WaveformPlaybackView-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6f5b1b3ce84e262eec2f5fea04e4d8d15d0ec4dabd589b73b8b0c0b41afb514 -size 9754 +oid sha256:876ac7c1ac9f3a188bd4ebcc4faef6fd363e166b1ec3d5a844872f7a0cf676aa +size 9969 From 1fc0b8f45d09bcf00579b09d2846b8e90d27179a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 21:47:50 +0000 Subject: [PATCH 201/281] Update dependency com.google.firebase:firebase-appdistribution-gradle to v4.0.1 --- plugins/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index a64b822d62..9986d490fa 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -28,6 +28,6 @@ dependencies { implementation(libs.kotlin.gradle.plugin) implementation(platform(libs.google.firebase.bom)) // FIXME: using the bom ^, it should not be necessary to provide the version v... - implementation("com.google.firebase:firebase-appdistribution-gradle:4.0.0") + implementation("com.google.firebase:firebase-appdistribution-gradle:4.0.1") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } From 1389c9ed245000921bf96832653cb7a4e1f6b243 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 26 Oct 2023 23:46:03 +0100 Subject: [PATCH 202/281] Simple live waveform implementation. --- .../composer/VoiceMessageComposerPresenter.kt | 3 +- .../VoiceMessageComposerStateProvider.kt | 7 +- .../media/DrawScopeWaveformExtensions.kt | 57 ++++++++++ .../components/media/WaveformPlaybackView.kt | 25 ++--- .../libraries/textcomposer/TextComposer.kt | 5 +- .../components/LiveWaveformView.kt | 106 ++++++++++++++++++ .../components/VoiceMessageRecording.kt | 32 ++---- .../textcomposer/model/VoiceMessageState.kt | 3 +- .../voicerecorder/api/VoiceRecorderState.kt | 4 +- .../voicerecorder/impl/VoiceRecorderImpl.kt | 4 +- 10 files changed, 195 insertions(+), 51 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/DrawScopeWaveformExtensions.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index 5e0d9bcada..6500176e73 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -40,6 +40,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber @@ -172,7 +173,7 @@ class VoiceMessageComposerPresenter @Inject constructor( voiceMessageState = when (val state = recorderState) { is VoiceRecorderState.Recording -> VoiceMessageState.Recording( duration = state.elapsedTime, - level = state.level + levels = state.levels.toPersistentList() ) is VoiceRecorderState.Finished -> if (isSending) { VoiceMessageState.Sending diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt index c448ca1a84..f7e3263287 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt @@ -18,12 +18,13 @@ package io.element.android.features.messages.impl.voicemessages.composer import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.textcomposer.model.VoiceMessageState +import kotlinx.collections.immutable.toPersistentList import kotlin.time.Duration.Companion.seconds internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, level = 0.5f)), + aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, levels = aWaveformLevels)), ) } @@ -35,3 +36,7 @@ internal fun aVoiceMessageComposerState( showPermissionRationaleDialog = showPermissionRationaleDialog, eventSink = {}, ) + +internal var aWaveformLevels = List(100) { it.toFloat() / 200 }.toPersistentList() + + diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/DrawScopeWaveformExtensions.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/DrawScopeWaveformExtensions.kt new file mode 100644 index 0000000000..9e09d97471 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/DrawScopeWaveformExtensions.kt @@ -0,0 +1,57 @@ +/* + * 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.components.media + +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import kotlin.math.max + +fun DrawScope.drawWaveform( + waveformData: ImmutableList, + canvasSize: DpSize, + brush: Brush, + minimumGraphAmplitude: Float = 2F, + lineWidth: Dp = 2.dp, + linePadding: Dp = 2.dp, +) { + val centerY = canvasSize.height.toPx() / 2 + val cornerRadius = lineWidth / 2 + waveformData.forEachIndexed { index, amplitude -> + val drawingAmplitude = max(minimumGraphAmplitude, amplitude * (canvasSize.height.toPx() - 2)) + drawRoundRect( + brush = brush, + topLeft = Offset( + x = index * (linePadding + lineWidth).toPx(), + y = centerY - drawingAmplitude / 2 + ), + size = Size( + width = lineWidth.toPx(), + height = drawingAmplitude + ), + cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx()), + style = Fill + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index d9d15c1c3b..5cf92bf28a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -48,7 +48,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlin.math.max private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F @OptIn(ExperimentalComposeUiApi::class) @@ -64,7 +63,6 @@ fun WaveformPlaybackView( cursorBrush: Brush = SolidColor(ElementTheme.colors.iconAccentTertiary), lineWidth: Dp = 2.dp, linePadding: Dp = 2.dp, - minimumGraphAmplitude: Float = 2F, ) { val seekProgress = remember { mutableStateOf(null) } var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) } @@ -121,22 +119,13 @@ fun WaveformPlaybackView( canvasSizePx = size val centerY = canvasSize.height.toPx() / 2 val cornerRadius = lineWidth / 2 - normalizedWaveformData.forEachIndexed { index, amplitude -> - val drawingAmplitude = max(minimumGraphAmplitude, amplitude * (canvasSize.height.toPx() - 2)) - drawRoundRect( - brush = brush, - topLeft = Offset( - x = index * (linePadding + lineWidth).toPx(), - y = centerY - drawingAmplitude / 2 - ), - size = Size( - width = lineWidth.toPx(), - height = drawingAmplitude - ), - cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx()), - style = Fill - ) - } + drawWaveform( + waveformData = normalizedWaveformData, + canvasSize = canvasSize, + brush = brush, + lineWidth = lineWidth, + linePadding = linePadding + ) drawRect( brush = progressBrush, size = Size( 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 73c1764de2..8432700f3d 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 @@ -82,6 +82,7 @@ import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlin.time.Duration.Companion.seconds @Composable @@ -204,7 +205,7 @@ fun TextComposer( onPauseClick = onPauseVoiceMessageClicked ) is VoiceMessageState.Recording -> - VoiceMessageRecording(voiceMessageState.level, voiceMessageState.duration) + VoiceMessageRecording(voiceMessageState.levels, voiceMessageState.duration) VoiceMessageState.Idle -> {} } } @@ -798,7 +799,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { enableVoiceMessages = true, ) PreviewColumn(items = persistentListOf({ - VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5f)) + VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, List(100) { it.toFloat() / 200 }.toPersistentList())) }, { VoicePreview(voiceMessageState = VoiceMessageState.Preview(isPlaying = false)) }, { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt new file mode 100644 index 0000000000..aa4a6cc749 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt @@ -0,0 +1,106 @@ +/* + * 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.textcomposer.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.media.drawWaveform +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F +private val waveFormHeight = 26.dp +@Composable +fun LiveWaveformView( + levels: ImmutableList, + modifier: Modifier = Modifier, + brush: Brush = SolidColor(ElementTheme.colors.iconQuaternary), + lineWidth: Dp = 2.dp, + linePadding: Dp = 2.dp, +) { + var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) } + + val canvasWidth by remember(levels, lineWidth, linePadding) { + derivedStateOf { + levels.size * (lineWidth.value + linePadding.value) + } + } + var width by remember { mutableIntStateOf(0) } + + Box(contentAlignment = Alignment.CenterEnd, + modifier = modifier + .fillMaxWidth() + .height(waveFormHeight) + .onSizeChanged { width = it.width } + ) { + Canvas( + modifier = Modifier + .width(Dp(canvasWidth)) + .graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA) + .then(modifier) + ) { + canvasSize = DpSize(Dp(canvasWidth), size.height.toDp()) + val countThatFitsWidth = (width.toFloat() / (lineWidth.toPx() + linePadding.toPx())).toInt() + drawWaveform( + waveformData = levels.takeLast(countThatFitsWidth).toPersistentList(), + canvasSize = canvasSize, + brush = brush, + lineWidth = lineWidth, + linePadding = linePadding, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun LiveWaveformViewPreview() = ElementPreview { + Column { + + LiveWaveformView( + levels = List(100) { it.toFloat() / 200 }.toPersistentList(), + modifier = Modifier.height(34.dp), + ) + LiveWaveformView( + levels = List(40) { it.toFloat() / 40 }.toPersistentList(), + modifier = Modifier.height(34.dp), + ) + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 22244cd58b..29993a0ec6 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -37,12 +36,14 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.utils.time.formatShort +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @Composable internal fun VoiceMessageRecording( - level: Float, + levels: ImmutableList, duration: Duration, modifier: Modifier = Modifier, ) { @@ -70,28 +71,11 @@ internal fun VoiceMessageRecording( Spacer(Modifier.size(20.dp)) - // TODO Replace with waveform UI - DebugAudioLevel( - modifier = Modifier.weight(1f), level = level - ) - } -} - -@Composable -private fun DebugAudioLevel( - level: Float, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .height(26.dp) - ) { - Box( + LiveWaveformView( modifier = Modifier - .align(Alignment.CenterEnd) - .fillMaxWidth(level) - .background(ElementTheme.colors.iconQuaternary, shape = MaterialTheme.shapes.small) - .fillMaxHeight() + .height(34.dp) + .weight(1f), + levels = levels ) } } @@ -108,5 +92,5 @@ private fun RedRecordingDot( @PreviewsDayNight @Composable internal fun VoiceMessageRecordingPreview() = ElementPreview { - VoiceMessageRecording(0.5f, 0.seconds) + VoiceMessageRecording(List(100) { it.toFloat() / 200 }.toPersistentList(), 0.seconds) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index 012f655ad2..e5edd2c760 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.textcomposer.model +import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration sealed class VoiceMessageState { @@ -27,6 +28,6 @@ sealed class VoiceMessageState { data object Sending: VoiceMessageState() data class Recording( val duration: Duration, - val level: Float, + val levels: ImmutableList, ): VoiceMessageState() } diff --git a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt index c168e3d5fe..6f7ac54f5e 100644 --- a/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt +++ b/libraries/voicerecorder/api/src/main/kotlin/io/element/android/libraries/voicerecorder/api/VoiceRecorderState.kt @@ -29,9 +29,9 @@ sealed class VoiceRecorderState { * The recorder is currently recording. * * @property elapsedTime The elapsed time since the recording started. - * @property level The current audio level of the recording as a fraction of 1. + * @property levels The current audio levels of the recording as a fraction of 1. */ - data class Recording(val elapsedTime: Duration, val level: Float) : VoiceRecorderState() + data class Recording(val elapsedTime: Duration, val levels: List) : VoiceRecorderState() /** * The recorder has finished recording. diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt index e1481083f9..2590cf64e0 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -96,13 +96,13 @@ class VoiceRecorderImpl @Inject constructor( when (audio) { is Audio.Data -> { val audioLevel = audioLevelCalculator.calculateAudioLevel(audio.buffer) - _state.emit(VoiceRecorderState.Recording(elapsedTime, audioLevel)) levels.add(audioLevel) + _state.emit(VoiceRecorderState.Recording(elapsedTime, levels)) encoder.encode(audio.buffer, audio.readSize) } is Audio.Error -> { Timber.e("Voice message error: code=${audio.audioRecordErrorCode}") - _state.emit(VoiceRecorderState.Recording(elapsedTime, 0.0f)) + _state.emit(VoiceRecorderState.Recording(elapsedTime, listOf())) } } } From a7cfb610b19e3b9d9875169338475aef232e55af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 07:37:33 +0200 Subject: [PATCH 203/281] Update dependency com.google.firebase:firebase-bom to v32.4.1 (#1657) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d17bcf36bc..41eba3a713 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.4.0" +google_firebase_bom = "com.google.firebase:firebase-bom:32.4.1" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } From 515dca86b67df7520d851c206c4660af0aa7264c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 08:12:44 +0200 Subject: [PATCH 204/281] Update plugin com.google.firebase.appdistribution to v4.0.1 (#1656) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 324da8447d..c8e11927d1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,7 +27,7 @@ plugins { alias(libs.plugins.anvil) alias(libs.plugins.ksp) alias(libs.plugins.kapt) - id("com.google.firebase.appdistribution") version "4.0.0" + id("com.google.firebase.appdistribution") version "4.0.1" id("org.jetbrains.kotlinx.knit") version "0.4.0" id("kotlin-parcelize") // To be able to update the firebase.xml files, uncomment and build the project From 416aa3b64a6754abde1fea59e5c82c8f0cef220a Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 27 Oct 2023 08:41:08 +0100 Subject: [PATCH 205/281] FIx height and concurrent exception - Fix live waveform hight - Fix concurrent modification exception when sharing levels - Tidy up width of live waveform --- .../components/LiveWaveformView.kt | 7 +-- .../components/VoiceMessageRecording.kt | 2 +- .../voicerecorder/impl/VoiceRecorderImpl.kt | 45 ++++++++++++------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt index aa4a6cc749..0d083fe3e2 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt @@ -44,6 +44,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList +import java.lang.Float.min private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F private val waveFormHeight = 26.dp @@ -57,7 +58,7 @@ fun LiveWaveformView( ) { var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) } - val canvasWidth by remember(levels, lineWidth, linePadding) { + val waveformWidth by remember(levels, lineWidth, linePadding) { derivedStateOf { levels.size * (lineWidth.value + linePadding.value) } @@ -72,11 +73,11 @@ fun LiveWaveformView( ) { Canvas( modifier = Modifier - .width(Dp(canvasWidth)) + .width(canvasSize.width) .graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA) .then(modifier) ) { - canvasSize = DpSize(Dp(canvasWidth), size.height.toDp()) + canvasSize = DpSize(Dp(min(waveformWidth, width.toFloat())), size.height.toDp()) val countThatFitsWidth = (width.toFloat() / (lineWidth.toPx() + linePadding.toPx())).toInt() drawWaveform( waveformData = levels.takeLast(countThatFitsWidth).toPersistentList(), diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 29993a0ec6..038864d4da 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -73,7 +73,7 @@ internal fun VoiceMessageRecording( LiveWaveformView( modifier = Modifier - .height(34.dp) + .height(26.dp) .weight(1f), levels = levels ) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt index 2590cf64e0..0dcd085a81 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.libraries.voicerecorder.impl.audio.Audio @@ -38,6 +39,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.yield import timber.log.Timber import java.io.File @@ -67,6 +70,7 @@ class VoiceRecorderImpl @Inject constructor( private var audioReader: AudioReader? = null private var recordingJob: Job? = null private val levels: MutableList = mutableListOf() + private val lock = Mutex() private val _state = MutableStateFlow(VoiceRecorderState.Idle) override val state: StateFlow = _state @@ -76,7 +80,10 @@ class VoiceRecorderImpl @Inject constructor( Timber.i("Voice recorder started recording") outputFile = fileManager.createFile() .also(encoder::init) - levels.clear() + + lock.withLock { + levels.clear() + } val audioRecorder = audioReaderFactory.create(config, dispatchers).also { audioReader = it } @@ -96,8 +103,11 @@ class VoiceRecorderImpl @Inject constructor( when (audio) { is Audio.Data -> { val audioLevel = audioLevelCalculator.calculateAudioLevel(audio.buffer) - levels.add(audioLevel) - _state.emit(VoiceRecorderState.Recording(elapsedTime, levels)) + + lock.withLock{ + levels.add(audioLevel) + _state.emit(VoiceRecorderState.Recording(elapsedTime, levels.toList())) + } encoder.encode(audio.buffer, audio.readSize) } is Audio.Error -> { @@ -126,21 +136,24 @@ class VoiceRecorderImpl @Inject constructor( audioReader = null encoder.release() - if (cancelled) { - deleteRecording() - levels.clear() - } - _state.emit( - when (val file = outputFile) { - null -> VoiceRecorderState.Idle - else -> VoiceRecorderState.Finished( - file = file, - mimeType = fileConfig.mimeType, - waveform = levels.resample(100), - ) + lock.withLock { + if (cancelled) { + deleteRecording() + levels.clear() } - ) + + _state.emit( + when (val file = outputFile) { + null -> VoiceRecorderState.Idle + else -> VoiceRecorderState.Finished( + file = file, + mimeType = fileConfig.mimeType, + waveform = levels.resample(100), + ) + } + ) + } } /** From 1a2b728361c23580e18f2aebaeecfcdfdcc5a63b Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 27 Oct 2023 08:45:20 +0100 Subject: [PATCH 206/281] lint --- .../android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt index 0dcd085a81..89e6f9186b 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImpl.kt @@ -23,7 +23,6 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.libraries.voicerecorder.impl.audio.Audio From b5586cb7d8ad73e933efb333dc6e520aa73a837d Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 09:58:17 +0200 Subject: [PATCH 207/281] Move MediaPlayer from messages module to new dedicated library module (#1654) --- features/messages/impl/build.gradle.kts | 2 + .../features/messages/impl/MessagesNode.kt | 2 +- .../composer/VoiceMessageComposerPlayer.kt | 2 +- .../timeline/VoiceMessagePlayer.kt | 2 +- .../messages/MessagesPresenterTest.kt | 2 +- .../VoiceMessageComposerPresenterTest.kt | 2 +- .../timeline/DefaultVoiceMessagePlayerTest.kt | 4 +- .../timeline/VoiceMessagePresenterTest.kt | 2 +- libraries/mediaplayer/api/build.gradle.kts | 32 ++++++++ .../libraries/mediaplayer/api/MediaPlayer.kt | 78 +++++++++++++++++++ libraries/mediaplayer/impl/build.gradle.kts | 45 +++++++++++ .../mediaplayer/impl/MediaPlayerImpl.kt | 62 +-------------- .../mediaplayer/impl}/SimplePlayer.kt | 2 +- .../mediaplayer/impl/MediaPlayerImplTest.kt | 27 +++++++ libraries/mediaplayer/test/build.gradle.kts | 31 ++++++++ .../mediaplayer/test}/FakeMediaPlayer.kt | 4 +- .../kotlin/extension/DependencyHandleScope.kt | 1 + 17 files changed, 229 insertions(+), 71 deletions(-) create mode 100644 libraries/mediaplayer/api/build.gradle.kts create mode 100644 libraries/mediaplayer/api/src/main/kotlin/io/element/android/libraries/mediaplayer/api/MediaPlayer.kt create mode 100644 libraries/mediaplayer/impl/build.gradle.kts rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/MediaPlayer.kt => libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImpl.kt (76%) rename {features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer => libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl}/SimplePlayer.kt (97%) create mode 100644 libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImplTest.kt create mode 100644 libraries/mediaplayer/test/build.gradle.kts rename {features/messages/impl/src/test/kotlin/io/element/android/features/messages/mediaplayer => libraries/mediaplayer/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test}/FakeMediaPlayer.kt (93%) diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 814262321c..ca3051351f 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.voicerecorder.api) + implementation(projects.libraries.mediaplayer.api) implementation(projects.libraries.uiUtils) implementation(projects.features.networkmonitor.api) implementation(projects.services.analytics.api) @@ -83,6 +84,7 @@ dependencies { testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.textcomposer.test) testImplementation(projects.libraries.voicerecorder.test) + testImplementation(projects.libraries.mediaplayer.test) testImplementation(libs.test.mockk) ksp(libs.showkase.processor) 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 2263fe51cb..636569a24b 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 @@ -31,7 +31,7 @@ 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.features.messages.impl.mediaplayer.MediaPlayer +import io.element.android.libraries.mediaplayer.api.MediaPlayer 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 diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt index 976351fd62..36dddb7d0d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt @@ -16,7 +16,7 @@ package io.element.android.features.messages.impl.voicemessages.composer -import io.element.android.features.messages.impl.mediaplayer.MediaPlayer +import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt index 3934c88b9c..93b336095f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt @@ -17,7 +17,7 @@ package io.element.android.features.messages.impl.voicemessages.timeline import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.messages.impl.mediaplayer.MediaPlayer +import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 6ab2e0d7f3..9709dde50e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -43,7 +43,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.media.FakeLocalMediaFactory -import io.element.android.features.messages.mediaplayer.FakeMediaPlayer +import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.features.messages.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 90ab3ae16e..276ea7a064 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -30,7 +30,7 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer -import io.element.android.features.messages.mediaplayer.FakeMediaPlayer +import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt index 397a4a0373..eac9a905bd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt @@ -18,10 +18,10 @@ package io.element.android.features.messages.voicemessages.timeline import app.cash.turbine.test import com.google.common.truth.Truth -import io.element.android.features.messages.impl.mediaplayer.MediaPlayer +import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.features.messages.impl.voicemessages.timeline.DefaultVoiceMessagePlayer import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageMediaRepo -import io.element.android.features.messages.mediaplayer.FakeMediaPlayer +import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.test.AN_EVENT_ID diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt index 3239c1879e..e735dd4f28 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt @@ -27,7 +27,7 @@ import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMes import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageMediaRepo import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessagePresenter import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState -import io.element.android.features.messages.mediaplayer.FakeMediaPlayer +import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/libraries/mediaplayer/api/build.gradle.kts b/libraries/mediaplayer/api/build.gradle.kts new file mode 100644 index 0000000000..383820b4e0 --- /dev/null +++ b/libraries/mediaplayer/api/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * 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") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.mediaplayer.api" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.libraries.matrix.api) + implementation(libs.coroutines.core) +} diff --git a/libraries/mediaplayer/api/src/main/kotlin/io/element/android/libraries/mediaplayer/api/MediaPlayer.kt b/libraries/mediaplayer/api/src/main/kotlin/io/element/android/libraries/mediaplayer/api/MediaPlayer.kt new file mode 100644 index 0000000000..2c72cbf54a --- /dev/null +++ b/libraries/mediaplayer/api/src/main/kotlin/io/element/android/libraries/mediaplayer/api/MediaPlayer.kt @@ -0,0 +1,78 @@ +/* + * 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.mediaplayer.api + +import io.element.android.libraries.matrix.api.core.EventId +import kotlinx.coroutines.flow.StateFlow + +/** + * A media player for Element X. + */ +interface MediaPlayer : AutoCloseable { + + /** + * The current state of the player. + */ + val state: StateFlow + + /** + * Acquires control of the player and starts playing the given media. + */ + fun acquireControlAndPlay( + uri: String, + mediaId: String, + mimeType: String, + ) + + /** + * Plays the current media. + */ + fun play() + + /** + * Pauses the current media. + */ + fun pause() + + /** + * Seeks the current media to the given position. + */ + fun seekTo(positionMs: Long) + + /** + * Releases any resources associated with this player. + */ + override fun close() + + data class State( + /** + * Whether the player is currently playing. + */ + val isPlaying: Boolean, + /** + * The id of the media which is currently playing. + * + * NB: This is usually the string representation of the [EventId] of the event + * which contains the media. + */ + val mediaId: String?, + /** + * The current position of the player. + */ + val currentPosition: Long, + ) +} diff --git a/libraries/mediaplayer/impl/build.gradle.kts b/libraries/mediaplayer/impl/build.gradle.kts new file mode 100644 index 0000000000..b71f031ba0 --- /dev/null +++ b/libraries/mediaplayer/impl/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * 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") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.mediaplayer.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + api(projects.libraries.mediaplayer.api) + implementation(libs.androidx.media3.exoplayer) + + implementation(libs.dagger) + implementation(projects.libraries.di) + + implementation(libs.coroutines.core) + + testImplementation(projects.tests.testutils) + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.mockk) + testImplementation(libs.test.turbine) + testImplementation(libs.coroutines.core) + testImplementation(libs.coroutines.test) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/MediaPlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImpl.kt similarity index 76% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/MediaPlayer.kt rename to libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImpl.kt index 0055496f43..88e592a9d8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/MediaPlayer.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImpl.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.mediaplayer +package io.element.android.libraries.mediaplayer.impl import androidx.media3.common.MediaItem import androidx.media3.common.Player import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -33,64 +33,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject -/** - * A media player for Element X. - */ -interface MediaPlayer : AutoCloseable { - - /** - * The current state of the player. - */ - val state: StateFlow - - /** - * Acquires control of the player and starts playing the given media. - */ - fun acquireControlAndPlay( - uri: String, - mediaId: String, - mimeType: String, - ) - - /** - * Plays the current media. - */ - fun play() - - /** - * Pauses the current media. - */ - fun pause() - - /** - * Seeks the current media to the given position. - */ - fun seekTo(positionMs: Long) - - /** - * Releases any resources associated with this player. - */ - override fun close() - - data class State( - /** - * Whether the player is currently playing. - */ - val isPlaying: Boolean, - /** - * The id of the media which is currently playing. - * - * NB: This is usually the string representation of the [EventId] of the event - * which contains the media. - */ - val mediaId: String?, - /** - * The current position of the player. - */ - val currentPosition: Long, - ) -} - /** * Default implementation of [MediaPlayer] backed by a [SimplePlayer]. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/SimplePlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/SimplePlayer.kt rename to libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt index aeaa7bd69f..79a51973e6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mediaplayer/SimplePlayer.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.mediaplayer +package io.element.android.libraries.mediaplayer.impl import android.content.Context import androidx.media3.common.MediaItem diff --git a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImplTest.kt b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImplTest.kt new file mode 100644 index 0000000000..bf111026e1 --- /dev/null +++ b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImplTest.kt @@ -0,0 +1,27 @@ +/* + * 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.mediaplayer.impl + +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MediaPlayerImplTest { + @Test + fun `default test`() = runTest { + // TODO + } +} diff --git a/libraries/mediaplayer/test/build.gradle.kts b/libraries/mediaplayer/test/build.gradle.kts new file mode 100644 index 0000000000..7f5654ccdf --- /dev/null +++ b/libraries/mediaplayer/test/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * 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.mediaplayer.test" +} + +dependencies { + api(projects.libraries.mediaplayer.api) + implementation(projects.tests.testutils) + + implementation(libs.coroutines.test) + implementation(libs.test.truth) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/mediaplayer/FakeMediaPlayer.kt b/libraries/mediaplayer/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeMediaPlayer.kt similarity index 93% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/mediaplayer/FakeMediaPlayer.kt rename to libraries/mediaplayer/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeMediaPlayer.kt index a9f0349552..efa5bb1fea 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/mediaplayer/FakeMediaPlayer.kt +++ b/libraries/mediaplayer/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeMediaPlayer.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.messages.mediaplayer +package io.element.android.libraries.mediaplayer.test -import io.element.android.features.messages.impl.mediaplayer.MediaPlayer +import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 33ba672bcd..28069b932c 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -103,6 +103,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:textcomposer:impl")) implementation(project(":libraries:cryptography:impl")) implementation(project(":libraries:voicerecorder:impl")) + implementation(project(":libraries:mediaplayer:impl")) } fun DependencyHandlerScope.allServicesImpl() { From 1cb27661c8cdc9419ed956c3629178458033194b Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 27 Oct 2023 09:32:05 +0100 Subject: [PATCH 208/281] Fix tests --- .../composer/VoiceMessageComposerPresenterTest.kt | 5 +++-- .../libraries/voicerecorder/test/FakeVoiceRecorder.kt | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 90ab3ae16e..27828446e7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -44,6 +44,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -66,7 +67,7 @@ class VoiceMessageComposerPresenterTest { companion object { private val RECORDING_DURATION = 1.seconds - private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, 0.2f) + private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toPersistentList()) } @Test @@ -145,7 +146,7 @@ class VoiceMessageComposerPresenterTest { } // Nothing should happen - assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(RECORDING_DURATION, 0.2f)) + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Recording(RECORDING_DURATION, RECORDING_STATE.levels)) voiceRecorder.assertCalls(started = 1, stopped = 0, deleted = 0) testPauseAndDestroy(finalState) diff --git a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt index 7d3f140529..e55d206594 100644 --- a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt +++ b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt @@ -53,8 +53,8 @@ class FakeVoiceRecorder( curRecording = File("file.ogg") timeSource += recordingDuration - levels.forEach { - _state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), it)) + for(i in 1..levels.size) { + _state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i))) } } From 1d1b97babdbc462436de0cd66cfb154e3bf84969 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 27 Oct 2023 08:51:51 +0000 Subject: [PATCH 209/281] Update screenshots --- ..._MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png | 4 ++-- ..._MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...nts_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png | 3 +++ ...nts_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png | 3 +++ ...onents_null_RecordButton-D-12_12_null,NEXUS_5,1.0,en].png} | 0 ...onents_null_RecordButton-N-12_13_null,NEXUS_5,1.0,en].png} | 0 ...mponents_null_SendButton-D-13_13_null,NEXUS_5,1.0,en].png} | 0 ...mponents_null_SendButton-N-13_14_null,NEXUS_5,1.0,en].png} | 0 ...ents_null_TextFormatting-D-14_14_null,NEXUS_5,1.0,en].png} | 0 ...ents_null_TextFormatting-N-14_15_null,NEXUS_5,1.0,en].png} | 0 ...VoiceMessageDeleteButton-D-15_15_null,NEXUS_5,1.0,en].png} | 0 ...VoiceMessageDeleteButton-N-15_16_null,NEXUS_5,1.0,en].png} | 0 ...null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png} | 0 ...null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png} | 0 ...ull_VoiceMessageRecording-D-16_16_null,NEXUS_5,1.0,en].png | 3 --- ...ull_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png | 3 +++ ...ull_VoiceMessageRecording-N-16_17_null,NEXUS_5,1.0,en].png | 3 --- ...ull_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png | 3 +++ ...oser_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 ++-- 20 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_RecordButton-D-11_11_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_RecordButton-D-12_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_RecordButton-N-11_12_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_RecordButton-N-12_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_SendButton-D-13_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_SendButton-N-13_14_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_TextFormatting-D-14_14_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_TextFormatting-N-14_15_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-14_14_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-15_15_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-14_15_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-15_16_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-16_16_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-16_17_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png index 9b798ded60..f860d06dd2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c25252b8d43f4ffb58673f375709b4811901a78676580a7dd0288b8624615d7 -size 7787 +oid sha256:0e250201e94c17c9907721753edf95250626511e20287a16576f0a7388a28bae +size 8123 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png index 26978d7efe..946624ce1b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55fa9c5633d3776a3401db72e2e53d9eacedd745e0db7f259f6ada5d7a8d584a -size 7473 +oid sha256:5e566ef57e3db6e25d86dab61492d6acd2712a063f0c70d76b049af6d0b84b41 +size 7750 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fae8a6fca3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 +size 4464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-12_12_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-11_11_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-D-12_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-12_13_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-11_12_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_RecordButton-N-12_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-13_13_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-12_12_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-D-13_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-13_14_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-12_13_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_SendButton-N-13_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-14_14_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-13_13_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-D-14_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-14_15_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-13_14_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_TextFormatting-N-14_15_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-14_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-15_15_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-14_14_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-D-15_15_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-14_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-15_16_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-14_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageDeleteButton-N-15_16_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-16_16_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 1436eea3f5..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-16_16_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:208ad62e23efd6f07e2cce08c1d1511af4c4fc2ae6bc3299134fed9efb1c55d3 -size 7238 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d4f45d7af7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0db8293a289a2566aa6929bdf2217bde9b73e80accc0a4e396366d4baeab097 +size 6834 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-16_17_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 4b2b719498..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-16_17_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8cf7662c33de6ad1b58785674cbfce1d6323fe84e35c8398b563ea65e5ff9fa7 -size 6919 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..14940aaaca --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92937df3d489e3c475d61c2ae6c5169a5fc8284607ff5e39c4e7cc6c82bb607b +size 6533 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index 1fc392e02d..2c2555bf7c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8eed5b8511637961df60be133688df984560cb84b333a9377e859446dfac2e04 -size 18036 +oid sha256:ed064889becfca73443372cd8508d26735f8f73413a3b400699a88dffd7f5380 +size 18383 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index 19603922a0..7177c6e8d0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14bcc3395316251b2e25a23f95b53030bf2e2ebe0623ef7e113330a5fd2f853d -size 15852 +oid sha256:21c3c1e7225f2f7c0b2aab809fe2bfa3c9327fcfeb35362a70485b66964bbc2a +size 16143 From b2a61f2ed3482f104b3c26c0369e5b665dd9ed07 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 11:30:57 +0200 Subject: [PATCH 210/281] Show correct UI when replying to a voice message (#1658) Shows voice messages in the room summary. Shows voice messages in the reply context menu and composer. Show replies to voice messages in the timeline. (before this PR voice messages were shown the same as audio messages) Story: https://github.com/vector-im/element-meta/issues/2106 --- .../messages/impl/MessagesPresenter.kt | 5 ++++- .../actionlist/ActionListStateProvider.kt | 18 +++++++++++++++ .../impl/actionlist/ActionListView.kt | 13 ++++++++++- .../components/TimelineItemEventRow.kt | 16 ++++++++++---- .../main/res/drawable/ic_voice_attachment.xml | 9 ++++++++ .../impl/DefaultRoomLastMessageFormatter.kt | 6 ++++- .../DefaultRoomLastMessageFormatterTest.kt | 22 ++++++++++++++++--- .../ui/components/AttachmentThumbnail.kt | 8 ++++++- ...tContent-D-1_1_null_10,NEXUS_5,1.0,en].png | 3 +++ ...etContent-D-1_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...etContent-D-1_1_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...etContent-D-1_1_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...etContent-D-1_1_null_9,NEXUS_5,1.0,en].png | 3 +++ ...tContent-N-1_2_null_10,NEXUS_5,1.0,en].png | 3 +++ ...etContent-N-1_2_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...etContent-N-1_2_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...etContent-N-1_2_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...etContent-N-1_2_null_9,NEXUS_5,1.0,en].png | 3 +++ 18 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 libraries/designsystem/src/main/res/drawable/ic_voice_attachment.xml create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_9,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_9,NEXUS_5,1.0,en].png 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 ea9eade4b3..f924e7f716 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 @@ -331,11 +331,14 @@ class MessagesPresenter @AssistedInject constructor( textContent = targetEvent.content.body, type = AttachmentThumbnailType.Audio, ) + is TimelineItemVoiceContent -> AttachmentThumbnailInfo( + textContent = targetEvent.content.body, + type = AttachmentThumbnailType.Voice, + ) is TimelineItemLocationContent -> AttachmentThumbnailInfo( type = AttachmentThumbnailType.Location, ) is TimelineItemPollContent, // TODO Polls: handle reply to - is TimelineItemVoiceContent, // TODO Voice messages: handle reply to is TimelineItemTextBasedContent, is TimelineItemRedactedContent, is TimelineItemStateContent, 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 44736bc076..3ea99aaede 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 @@ -20,11 +20,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -67,6 +69,22 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemActionList(), ) ), + anActionListState().copy( + target = ActionListState.Target.Success( + event = aTimelineItemEvent(content = aTimelineItemAudioContent()).copy( + reactionsState = reactionsState + ), + actions = aTimelineItemActionList(), + ) + ), + anActionListState().copy( + target = ActionListState.Target.Success( + event = aTimelineItemEvent(content = aTimelineItemVoiceContent()).copy( + reactionsState = reactionsState + ), + actions = aTimelineItemActionList(), + ) + ), anActionListState().copy( target = ActionListState.Target.Success( event = aTimelineItemEvent(content = aTimelineItemLocationContent()).copy( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index c5e6e5facd..bed3af59bd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -238,7 +238,6 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif when (event.content) { is TimelineItemPollContent, // TODO Polls: handle summary - is TimelineItemVoiceContent, // TODO Voice messages: handle reply summary is TimelineItemTextBasedContent, is TimelineItemStateContent, is TimelineItemEncryptedContent, @@ -309,6 +308,18 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif } content = { ContentForBody(event.content.body) } } + is TimelineItemVoiceContent -> { + icon = { + AttachmentThumbnail( + modifier = imageModifier, + info = AttachmentThumbnailInfo( + textContent = textContent, + type = AttachmentThumbnailType.Voice, + ) + ) + } + content = { ContentForBody(event.content.body) } + } } Row(modifier = modifier) { icon() 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 0ef1c9bc67..304ff88662 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 @@ -612,10 +612,18 @@ private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready): Att textContent = messageContent.body, type = AttachmentThumbnailType.Location, ) - is AudioMessageType -> AttachmentThumbnailInfo( - textContent = messageContent.body, - type = AttachmentThumbnailType.Audio, - ) + is AudioMessageType -> { + when (type.isVoiceMessage) { + true -> AttachmentThumbnailInfo( + textContent = messageContent.body, + type = AttachmentThumbnailType.Voice, + ) + false -> AttachmentThumbnailInfo( + textContent = messageContent.body, + type = AttachmentThumbnailType.Audio, + ) + } + } else -> null } } diff --git a/libraries/designsystem/src/main/res/drawable/ic_voice_attachment.xml b/libraries/designsystem/src/main/res/drawable/ic_voice_attachment.xml new file mode 100644 index 0000000000..d8986e206d --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_voice_attachment.xml @@ -0,0 +1,9 @@ + + + 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 80e7a7155b..99b06401d9 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 @@ -128,7 +128,11 @@ class DefaultRoomLastMessageFormatter @Inject constructor( sp.getString(CommonStrings.common_file) } is AudioMessageType -> { - sp.getString(CommonStrings.common_audio) + if (messageType.isVoiceMessage) { + sp.getString(CommonStrings.common_voice_message) + } else { + sp.getString(CommonStrings.common_audio) + } } is OtherMessageType -> { messageType.body 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 bc58aa1d48..48c498c3d8 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 @@ -163,6 +163,7 @@ class DefaultRoomLastMessageFormatterTest { TextMessageType(body, null), VideoMessageType(body, MediaSource("url"), null), AudioMessageType(body, MediaSource("url"), null, null, false), + AudioMessageType(body, MediaSource("url"), null, null, true), ImageMessageType(body, MediaSource("url"), null), FileMessageType(body, MediaSource("url"), null), LocationMessageType(body, "geo:1,2", null), @@ -198,7 +199,12 @@ class DefaultRoomLastMessageFormatterTest { for ((type, result) in resultsInDm) { val expectedResult = when (type) { is VideoMessageType -> "Video" - is AudioMessageType -> "Audio" + is AudioMessageType -> { + when (type.isVoiceMessage) { + true -> "Voice message" + false -> "Audio" + } + } is ImageMessageType -> "Image" is FileMessageType -> "File" is LocationMessageType -> "Shared location" @@ -216,7 +222,12 @@ class DefaultRoomLastMessageFormatterTest { val string = result.toString() val expectedResult = when (type) { is VideoMessageType -> "$senderName: Video" - is AudioMessageType -> "$senderName: Audio" + is AudioMessageType -> { + when (type.isVoiceMessage) { + true -> "$senderName: Voice message" + false -> "$senderName: Audio" + } + } is ImageMessageType -> "$senderName: Image" is FileMessageType -> "$senderName: File" is LocationMessageType -> "$senderName: Shared location" @@ -228,7 +239,12 @@ class DefaultRoomLastMessageFormatterTest { } val shouldCreateAnnotatedString = when (type) { is VideoMessageType -> true - is AudioMessageType -> true + is AudioMessageType -> { + when (type.isVoiceMessage) { + true -> true + false -> true + } + } is ImageMessageType -> true is FileMessageType -> true is LocationMessageType -> false diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt index cafbdd04c0..c1fa34a764 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt @@ -75,6 +75,12 @@ fun AttachmentThumbnail( contentDescription = info.textContent, ) } + AttachmentThumbnailType.Voice -> { + Icon( + resourceId = CommonDrawables.ic_voice_attachment, + contentDescription = info.textContent, + ) + } AttachmentThumbnailType.File -> { Icon( resourceId = CommonDrawables.ic_september_attachment, @@ -95,7 +101,7 @@ fun AttachmentThumbnail( @Parcelize enum class AttachmentThumbnailType : Parcelable { - Image, Video, File, Audio, Location + Image, Video, File, Audio, Location, Voice } @Parcelize diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af7782f5d7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85692ea3847fe5a79f955bb71153ccb6e5b24cf451292c7b9d56f26e2eff95b7 +size 28840 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png index 74448d6967..c458b9a7b8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:979138753000ada021a67f4bc14a89b912430c4331ac69aa702b2e50c050d7cb -size 41200 +oid sha256:d32becec465a3f01e35aac9ebbeb8f48507cc2146f35b8e6c6393bbd410cca3e +size 39370 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png index 3cb12c4c94..0ed5c052a6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e06e1a32d8c9d06a307a5116a396a03308d5e5486f911d34002139485e7d9ffe -size 28176 +oid sha256:a1f8750e66aa2a60198fe68d2e3185bcf5a8708df81502f33c394d72f7b20a30 +size 42765 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_8,NEXUS_5,1.0,en].png index af7782f5d7..74448d6967 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85692ea3847fe5a79f955bb71153ccb6e5b24cf451292c7b9d56f26e2eff95b7 -size 28840 +oid sha256:979138753000ada021a67f4bc14a89b912430c4331ac69aa702b2e50c050d7cb +size 41200 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3cb12c4c94 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e06e1a32d8c9d06a307a5116a396a03308d5e5486f911d34002139485e7d9ffe +size 28176 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ee2fa7b46f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5df2d3b6e4698867277712214fa526e08a9bf790140cd000cfe2bf94c70e77f4 +size 27525 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png index 8d45efac8c..dbe1f4d267 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752abe37a810cc8e4b36ae5f4c4aa38c9f22757b6458f272ba991050c69aee53 -size 39409 +oid sha256:a99566280864e66ec327d2d6d8016c74b12f1c62ff27fd0d2d4ba0109ae9756a +size 37747 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png index 811cb7e4cd..8ee1838413 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c53fdbe427ddb7088d9bd8f9b46a1281d145fd6f9ccc3a7de339750e3079075 -size 26498 +oid sha256:fbb1233cf288e6ffc1aaa7136b56c412699897d7edd20bef82784562e302bf40 +size 41215 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_8,NEXUS_5,1.0,en].png index ee2fa7b46f..8d45efac8c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5df2d3b6e4698867277712214fa526e08a9bf790140cd000cfe2bf94c70e77f4 -size 27525 +oid sha256:752abe37a810cc8e4b36ae5f4c4aa38c9f22757b6458f272ba991050c69aee53 +size 39409 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..811cb7e4cd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c53fdbe427ddb7088d9bd8f9b46a1281d145fd6f9ccc3a7de339750e3079075 +size 26498 From 8321eaf19de5322d3fa815f83e01330c292077b6 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 27 Oct 2023 10:56:45 +0100 Subject: [PATCH 211/281] Fix tests --- .../voicerecorder/impl/VoiceRecorderImplTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt index cb9881087e..b764dc92f7 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/VoiceRecorderImplTest.kt @@ -60,11 +60,11 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) voiceRecorder.startRecord() - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, 1.0f)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.seconds, listOf(1.0f))) timeSource += 1.seconds - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds, 0.0f)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(1.seconds, listOf())) timeSource += 1.seconds - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, 1.0f)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(2.seconds, listOf(1.0f, 1.0f))) } } @@ -75,9 +75,9 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Idle) voiceRecorder.startRecord() - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.minutes, 1.0f)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(0.minutes, listOf(1.0f))) timeSource += 29.minutes - assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, 0.0f)) + assertThat(awaitItem()).isEqualTo(VoiceRecorderState.Recording(29.minutes, listOf())) timeSource += 1.minutes assertThat(awaitItem()).isEqualTo( From 9807ebf649afef73a1bbbf1931fe4afb70b5ce4d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Oct 2023 11:14:00 +0200 Subject: [PATCH 212/281] Secure backup --- .idea/dictionaries/shared.xml | 1 + .maestro/tests/account/logout.yaml | 9 +- .../android/appnav/LoggedInFlowNode.kt | 12 + .../features/logout/api/LogoutEntryPoint.kt | 35 +++ .../logout/api/LogoutPreferenceView.kt | 94 -------- features/logout/impl/build.gradle.kts | 1 + .../logout/impl/DefaultLogoutEntryPoint.kt | 46 ++++ .../impl/DefaultLogoutPreferencePresenter.kt | 64 ----- .../features/logout/impl/LogoutEvents.kt} | 7 +- .../features/logout/impl/LogoutNode.kt | 64 +++++ .../features/logout/impl/LogoutPresenter.kt | 92 +++++++ .../features/logout/impl/LogoutState.kt | 28 +++ .../logout/impl/LogoutStateProvider.kt | 47 ++++ .../features/logout/impl/LogoutView.kt | 224 ++++++++++++++++++ .../src/main/res/values-cs/translations.xml | 0 .../src/main/res/values-de/translations.xml | 0 .../src/main/res/values-es/translations.xml | 0 .../src/main/res/values-fr/translations.xml | 0 .../src/main/res/values-it/translations.xml | 0 .../src/main/res/values-ro/translations.xml | 0 .../src/main/res/values-ru/translations.xml | 0 .../src/main/res/values-sk/translations.xml | 0 .../main/res/values-zh-rTW/translations.xml | 0 .../src/main/res/values/localazy.xml | 0 .../logout/impl/src/main/res/values/tmp.xml | 6 + .../impl/LogoutPreferencePresenterTest.kt | 87 ------- .../logout/impl/LogoutPresenterTest.kt | 199 ++++++++++++++++ .../preferences/api/PreferencesEntryPoint.kt | 1 + features/preferences/impl/build.gradle.kts | 2 + .../preferences/impl/PreferencesFlowNode.kt | 23 ++ .../impl/root/PreferencesRootNode.kt | 21 +- .../impl/root/PreferencesRootPresenter.kt | 10 +- .../impl/root/PreferencesRootState.kt | 4 +- .../impl/root/PreferencesRootStateProvider.kt | 4 +- .../impl/root/PreferencesRootView.kt | 26 +- .../impl/root/PreferencesRootPresenterTest.kt | 16 +- features/roomlist/impl/build.gradle.kts | 2 + .../features/roomlist/impl/RoomListEvents.kt | 1 + .../roomlist/impl/RoomListPresenter.kt | 18 ++ .../features/roomlist/impl/RoomListState.kt | 2 + .../roomlist/impl/RoomListStateProvider.kt | 11 +- .../features/roomlist/impl/RoomListView.kt | 9 + .../components/ConfirmRecoveryKeyBanner.kt | 49 ++++ .../impl/components/RoomListTopBar.kt | 30 ++- .../impl/src/main/res/values/localazy.xml | 2 + .../roomlist/impl/RoomListPresenterTests.kt | 44 +++- features/securebackup/api/build.gradle.kts | 27 +++ .../api/SecureBackupEntryPoint.kt} | 6 +- features/securebackup/impl/build.gradle.kts | 56 +++++ .../impl/DefaultSecureBackupEntryPoint.kt | 32 +++ .../features/securebackup/impl/LoggerTag.kt | 25 ++ .../securebackup/impl/SecureBackupConfig.kt | 22 ++ .../securebackup/impl/SecureBackupFlowNode.kt | 133 +++++++++++ .../impl/disable/SecureBackupDisableEvents.kt | 22 ++ .../impl/disable/SecureBackupDisableNode.kt | 45 ++++ .../disable/SecureBackupDisablePresenter.kt | 80 +++++++ .../impl/disable/SecureBackupDisableState.kt | 28 +++ .../SecureBackupDisableStateProvider.kt | 44 ++++ .../impl/disable/SecureBackupDisableView.kt | 162 +++++++++++++ .../impl/enable/SecureBackupEnableEvents.kt | 22 ++ .../impl/enable/SecureBackupEnableNode.kt | 45 ++++ .../enable/SecureBackupEnablePresenter.kt | 64 +++++ .../impl/enable/SecureBackupEnableState.kt} | 10 +- .../enable/SecureBackupEnableStateProvider.kt | 37 +++ .../impl/enable/SecureBackupEnableView.kt | 103 ++++++++ .../SecureBackupEnterRecoveryKeyEvents.kt | 23 ++ .../enter/SecureBackupEnterRecoveryKeyNode.kt | 64 +++++ .../SecureBackupEnterRecoveryKeyPresenter.kt | 86 +++++++ .../SecureBackupEnterRecoveryKeyState.kt | 28 +++ ...cureBackupEnterRecoveryKeyStateProvider.kt | 48 ++++ .../enter/SecureBackupEnterRecoveryKeyView.kt | 134 +++++++++++ .../impl/root/SecureBackupRootNode.kt | 91 +++++++ .../impl/root/SecureBackupRootPresenter.kt | 54 +++++ .../impl/root/SecureBackupRootState.kt | 28 +++ .../root/SecureBackupRootStateProvider.kt | 47 ++++ .../impl/root/SecureBackupRootView.kt | 141 +++++++++++ .../impl/setup/SecureBackupSetupEvents.kt | 24 ++ .../impl/setup/SecureBackupSetupNode.kt | 77 ++++++ .../impl/setup/SecureBackupSetupPresenter.kt | 142 +++++++++++ .../impl/setup/SecureBackupSetupState.kt | 41 ++++ .../setup/SecureBackupSetupStateMachine.kt | 70 ++++++ .../setup/SecureBackupSetupStateProvider.kt | 56 +++++ .../impl/setup/SecureBackupSetupView.kt | 207 ++++++++++++++++ .../SecureBackupSetupViewChangePreview.kt | 37 +++ .../impl/setup/views/RecoveryKeyView.kt | 221 +++++++++++++++++ .../impl/setup/views/RecoveryKeyViewState.kt | 29 +++ .../views/RecoveryKeyViewStateProvider.kt | 47 ++++ .../tools/RecoveryKeyVisualTransformation.kt | 51 ++++ .../src/main/res/values-sk/translations.xml | 42 ++++ .../impl/src/main/res/values/localazy.xml | 42 ++++ .../SecureBackupDisablePresenterTest.kt | 129 ++++++++++ .../enable/SecureBackupEnablePresenterTest.kt | 66 ++++++ ...cureBackupEnterRecoveryKeyPresenterTest.kt | 103 ++++++++ .../root/SecureBackupRootPresenterTest.kt | 59 +++++ .../setup/SecureBackupSetupPresenterTest.kt | 173 ++++++++++++++ .../RecoveryKeyVisualTransformationTest.kt | 74 ++++++ libraries/androidutils/build.gradle.kts | 2 +- .../components/preferences/PreferenceText.kt | 56 ++++- .../designsystem/modifiers/Clickable.kt | 28 +++ .../src/main/res/drawable/ic_key.xml | 9 + .../src/main/res/drawable/ic_key_filled.xml | 25 ++ .../src/main/res/drawable/ic_key_off.xml | 9 + .../indicator/api/build.gradle.kts | 15 +- .../indicator/api/IndicatorService.kt | 31 +++ libraries/indicator/impl/build.gradle.kts | 46 ++++ .../indicator/impl/DefaultIndicatorService.kt | 70 ++++++ .../src/main/res/values-cs/translations.xml | 1 + .../src/main/res/values-ru/translations.xml | 1 + .../src/main/res/values-sk/translations.xml | 40 +--- .../src/main/res/values/localazy.xml | 44 +--- .../kotlin/extension/DependencyHandleScope.kt | 1 + samples/minimal/build.gradle.kts | 1 + .../android/samples/minimal/RoomListScreen.kt | 7 + .../android/tests/konsist/KonsistTestTest.kt | 4 +- tools/localazy/config.json | 13 +- 115 files changed, 4698 insertions(+), 393 deletions(-) create mode 100644 features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt delete mode 100644 features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceView.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt delete mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt rename features/logout/{api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceEvents.kt => impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvents.kt} (76%) create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt create mode 100644 features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt rename features/logout/{api => impl}/src/main/res/values-cs/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values-de/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values-es/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values-fr/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values-it/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values-ro/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values-ru/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values-sk/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values-zh-rTW/translations.xml (100%) rename features/logout/{api => impl}/src/main/res/values/localazy.xml (100%) create mode 100644 features/logout/impl/src/main/res/values/tmp.xml delete mode 100644 features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt create mode 100644 features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt create mode 100644 features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/ConfirmRecoveryKeyBanner.kt create mode 100644 features/securebackup/api/build.gradle.kts rename features/{logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferencePresenter.kt => securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt} (76%) create mode 100644 features/securebackup/impl/build.gradle.kts create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/LoggerTag.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupConfig.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvents.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableState.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStateProvider.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableEvents.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenter.kt rename features/{logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceState.kt => securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableState.kt} (73%) create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableStateProvider.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvents.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStateProvider.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvents.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateMachine.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateProvider.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewState.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStateProvider.kt create mode 100644 features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyVisualTransformation.kt create mode 100644 features/securebackup/impl/src/main/res/values-sk/translations.xml create mode 100644 features/securebackup/impl/src/main/res/values/localazy.xml create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenterTest.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenterTest.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt create mode 100644 features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyVisualTransformationTest.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Clickable.kt create mode 100644 libraries/designsystem/src/main/res/drawable/ic_key.xml create mode 100644 libraries/designsystem/src/main/res/drawable/ic_key_filled.xml create mode 100644 libraries/designsystem/src/main/res/drawable/ic_key_off.xml rename features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceStateProvider.kt => libraries/indicator/api/build.gradle.kts (73%) create mode 100644 libraries/indicator/api/src/main/kotlin/io/element/android/libraries/indicator/api/IndicatorService.kt create mode 100644 libraries/indicator/impl/build.gradle.kts create mode 100644 libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt diff --git a/.idea/dictionaries/shared.xml b/.idea/dictionaries/shared.xml index c3aa70ef23..7afe540d22 100644 --- a/.idea/dictionaries/shared.xml +++ b/.idea/dictionaries/shared.xml @@ -10,6 +10,7 @@ onboarding placeables posthog + securebackup showkase snackbar swipeable diff --git a/.maestro/tests/account/logout.yaml b/.maestro/tests/account/logout.yaml index a06ac25e2d..b0e8eda762 100644 --- a/.maestro/tests/account/logout.yaml +++ b/.maestro/tests/account/logout.yaml @@ -3,11 +3,12 @@ appId: ${APP_ID} - tapOn: id: "home_screen-settings" - tapOn: "Sign out" -- takeScreenshot: build/maestro/900-SignOutDialg +- takeScreenshot: build/maestro/900-SignOutScreen +- back +- tapOn: "Sign out" +- tapOn: "Sign out" # Ensure cancel cancels - tapOn: "Cancel" - tapOn: "Sign out" -- tapOn: - text: "Sign out" - index: 1 +- tapOn: "Sign out" - runFlow: ../assertions/assertInitDisplayed.yaml 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 c1eb17dec1..ed286c71b9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -55,6 +55,7 @@ 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.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.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -87,6 +88,7 @@ class LoggedInFlowNode @AssistedInject constructor( 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, @@ -197,6 +199,9 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data object VerifySession : NavTarget + @Parcelize + data object SecureBackup : NavTarget + @Parcelize data object InviteList : NavTarget @@ -272,6 +277,10 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.VerifySession) } + override fun onSecureBackupClicked() { + backstack.push(NavTarget.SecureBackup) + } + override fun onOpenRoomNotificationSettings(roomId: RoomId) { backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomNotificationSettings)) } @@ -297,6 +306,9 @@ class LoggedInFlowNode @AssistedInject constructor( NavTarget.VerifySession -> { verifySessionEntryPoint.createNode(this, buildContext) } + NavTarget.SecureBackup -> { + secureBackupEntryPoint.createNode(this, buildContext) + } NavTarget.InviteList -> { val callback = object : InviteListEntryPoint.Callback { override fun onBackClicked() { diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.kt new file mode 100644 index 0000000000..b750a47e6d --- /dev/null +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutEntryPoint.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.features.logout.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 LogoutEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onChangeRecoveryKeyClicked() + } +} diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceView.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceView.kt deleted file mode 100644 index 24b7c8a266..0000000000 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceView.kt +++ /dev/null @@ -1,94 +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.features.logout.api - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.res.stringResource -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.designsystem.components.ProgressDialog -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.components.preferences.PreferenceText -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.utils.CommonDrawables - -@Composable -fun LogoutPreferenceView( - state: LogoutPreferenceState, - onSuccessLogout: (logoutUrlResult: String?) -> Unit -) { - val eventSink = state.eventSink - if (state.logoutAction is Async.Success) { - LaunchedEffect(state.logoutAction) { - onSuccessLogout(state.logoutAction.data) - } - return - } - val openDialog = remember { mutableStateOf(false) } - - LogoutPreferenceContent( - onClick = { - openDialog.value = true - } - ) - - // Log out confirmation dialog - if (openDialog.value) { - ConfirmationDialog( - title = stringResource(id = R.string.screen_signout_confirmation_dialog_title), - content = stringResource(id = R.string.screen_signout_confirmation_dialog_content), - submitText = stringResource(id = R.string.screen_signout_confirmation_dialog_submit), - onCancelClicked = { - openDialog.value = false - }, - onSubmitClicked = { - openDialog.value = false - eventSink(LogoutPreferenceEvents.Logout) - }, - onDismiss = { - openDialog.value = false - } - ) - } - - if (state.logoutAction is Async.Loading) { - ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) - } -} - -@Composable -private fun LogoutPreferenceContent( - onClick: () -> Unit = {}, -) { - PreferenceText( - title = stringResource(id = R.string.screen_signout_preference_item), - iconResourceId = CommonDrawables.ic_compound_leave, - onClick = onClick - ) -} - -@PreviewsDayNight -@Composable -internal fun LogoutPreferenceViewPreview() = ElementPreview { - LogoutPreferenceView( - aLogoutPreferenceState(), - onSuccessLogout = {} - ) -} diff --git a/features/logout/impl/build.gradle.kts b/features/logout/impl/build.gradle.kts index f5ee8dd951..8680b355ae 100644 --- a/features/logout/impl/build.gradle.kts +++ b/features/logout/impl/build.gradle.kts @@ -31,6 +31,7 @@ anvil { dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt new file mode 100644 index 0000000000..4928850245 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutEntryPoint.kt @@ -0,0 +1,46 @@ +/* + * 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.logout.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.logout.api.LogoutEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultLogoutEntryPoint @Inject constructor() : LogoutEntryPoint { + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LogoutEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : LogoutEntryPoint.NodeBuilder { + + override fun callback(callback: LogoutEntryPoint.Callback): LogoutEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} + diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt deleted file mode 100644 index 49d0606633..0000000000 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/DefaultLogoutPreferencePresenter.kt +++ /dev/null @@ -1,64 +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.logout.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.logout.api.LogoutPreferenceEvents -import io.element.android.features.logout.api.LogoutPreferencePresenter -import io.element.android.features.logout.api.LogoutPreferenceState -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.MatrixClient -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ContributesBinding(SessionScope::class) -class DefaultLogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : - LogoutPreferencePresenter { - - @Composable - override fun present(): LogoutPreferenceState { - val localCoroutineScope = rememberCoroutineScope() - val logoutAction: MutableState> = remember { - mutableStateOf(Async.Uninitialized) - } - - fun handleEvents(event: LogoutPreferenceEvents) { - when (event) { - LogoutPreferenceEvents.Logout -> localCoroutineScope.logout(logoutAction) - } - } - - return LogoutPreferenceState( - logoutAction = logoutAction.value, - eventSink = ::handleEvents - ) - } - - private fun CoroutineScope.logout(logoutAction: MutableState>) = launch { - suspend { - matrixClient.logout(false /* TODO */) - }.runCatchingUpdatingState(logoutAction) - } -} diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceEvents.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvents.kt similarity index 76% rename from features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceEvents.kt rename to features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvents.kt index 50dad213fd..2a8ee322a1 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceEvents.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvents.kt @@ -14,8 +14,9 @@ * limitations under the License. */ -package io.element.android.features.logout.api +package io.element.android.features.logout.impl -sealed interface LogoutPreferenceEvents { - data object Logout : LogoutPreferenceEvents +sealed interface LogoutEvents { + data class Logout(val ignoreSdkError: Boolean) : LogoutEvents + data object CloseDialogs : LogoutEvents } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt new file mode 100644 index 0000000000..8c4a605222 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt @@ -0,0 +1,64 @@ +/* + * 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.logout.impl + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +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.logout.api.LogoutEntryPoint +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab +import io.element.android.libraries.di.SessionScope +import timber.log.Timber + +@ContributesNode(SessionScope::class) +class LogoutNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: LogoutPresenter, +) : Node(buildContext, plugins = plugins) { + + private fun onChangeRecoveryKeyClicked() { + plugins().forEach { it.onChangeRecoveryKeyClicked() } + } + + private fun onSuccessLogout(activity: Activity, url: String?) { + Timber.d("Success logout with result url: $url") + url?.let { + activity.openUrlInChromeCustomTab(null, false, it) + } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val activity = LocalContext.current as Activity + LogoutView( + state = state, + onChangeRecoveryKeyClicked = ::onChangeRecoveryKeyClicked, + onSuccessLogout = { onSuccessLogout(activity, it) }, + modifier = modifier, + ) + } +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt new file mode 100644 index 0000000000..8d97df3801 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.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.features.logout.impl + +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.setValue +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class LogoutPresenter @Inject constructor( + private val matrixClient: MatrixClient, + private val encryptionService: EncryptionService, +) : Presenter { + + @Composable + override fun present(): LogoutState { + val localCoroutineScope = rememberCoroutineScope() + val logoutAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + + val backupUploadState by encryptionService.backupUploadStateStateFlow.collectAsState() + + var showLogoutDialog by remember { mutableStateOf(false) } + var isLastSession by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + isLastSession = encryptionService.isLastDevice().getOrNull() ?: false + encryptionService.waitForBackupUploadSteadyState() + } + + fun handleEvents(event: LogoutEvents) { + when (event) { + is LogoutEvents.Logout -> { + if (showLogoutDialog || event.ignoreSdkError) { + showLogoutDialog = false + localCoroutineScope.logout(logoutAction, event.ignoreSdkError) + } else { + showLogoutDialog = true + } + } + LogoutEvents.CloseDialogs -> { + logoutAction.value = Async.Uninitialized + showLogoutDialog = false + } + } + } + + return LogoutState( + isLastSession = isLastSession, + backupUploadState = backupUploadState, + showConfirmationDialog = showLogoutDialog, + logoutAction = logoutAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.logout( + logoutAction: MutableState>, + ignoreSdkError: Boolean, + ) = launch { + suspend { + matrixClient.logout(ignoreSdkError) + }.runCatchingUpdatingState(logoutAction) + } +} diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt new file mode 100644 index 0000000000..1672640ab7 --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt @@ -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. + */ + +package io.element.android.features.logout.impl + +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.encryption.BackupUploadState + +data class LogoutState( + val isLastSession: Boolean, + val backupUploadState: BackupUploadState, + val showConfirmationDialog: Boolean, + val logoutAction: Async, + val eventSink: (LogoutEvents) -> Unit, +) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt new file mode 100644 index 0000000000..c0ceb5302b --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt @@ -0,0 +1,47 @@ +/* + * 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.logout.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.encryption.BackupUploadState + +open class LogoutStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLogoutState(), + aLogoutState(isLastSession = true), + aLogoutState(isLastSession = false, backupUploadState = BackupUploadState.Uploading(66, 200)), + aLogoutState(isLastSession = true, backupUploadState = BackupUploadState.Done), + aLogoutState(showConfirmationDialog = true), + aLogoutState(logoutAction = Async.Loading()), + aLogoutState(logoutAction = Async.Failure(Exception("Failed to logout"))), + ) +} + +fun aLogoutState( + isLastSession: Boolean = false, + backupUploadState: BackupUploadState = BackupUploadState.Unknown, + showConfirmationDialog: Boolean = false, + logoutAction: Async = Async.Uninitialized, +) = LogoutState( + isLastSession = isLastSession, + backupUploadState = backupUploadState, + showConfirmationDialog = showConfirmationDialog, + logoutAction = logoutAction, + eventSink = {} +) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt new file mode 100644 index 0000000000..d0635af31e --- /dev/null +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt @@ -0,0 +1,224 @@ +/* + * 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.logout.impl + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.architecture.Async +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.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +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.LinearProgressIndicator +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LogoutView( + state: LogoutState, + onChangeRecoveryKeyClicked: () -> Unit, + onSuccessLogout: (logoutUrlResult: String?) -> Unit, + modifier: Modifier = Modifier, +) { + val eventSink = state.eventSink + + HeaderFooterPage( + modifier = modifier, + header = { + HeaderContent(state = state) + }, + footer = { + BottomMenu( + state = state, + onChangeRecoveryKeyClicked = onChangeRecoveryKeyClicked, + onLogoutClicked = { + eventSink(LogoutEvents.Logout(ignoreSdkError = false)) + }, + ) + } + ) { + Content(state = state) + } + + // Log out confirmation dialog + if (state.showConfirmationDialog) { + ConfirmationDialog( + title = stringResource(id = CommonStrings.action_signout), + content = stringResource(id = R.string.screen_signout_confirmation_dialog_content), + submitText = stringResource(id = CommonStrings.action_signout), + onCancelClicked = { + eventSink(LogoutEvents.CloseDialogs) + }, + onSubmitClicked = { + eventSink(LogoutEvents.Logout(ignoreSdkError = false)) + }, + onDismiss = { + eventSink(LogoutEvents.CloseDialogs) + } + ) + } + + when (state.logoutAction) { + is Async.Loading -> + ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) + is Async.Failure -> + ConfirmationDialog( + title = stringResource(id = CommonStrings.dialog_title_error), + content = stringResource(id = CommonStrings.error_unknown), + submitText = stringResource(id = CommonStrings.action_signout_anyway), + onCancelClicked = { + eventSink(LogoutEvents.CloseDialogs) + }, + onSubmitClicked = { + eventSink(LogoutEvents.Logout(ignoreSdkError = true)) + }, + onDismiss = { + eventSink(LogoutEvents.CloseDialogs) + } + ) + Async.Uninitialized -> + Unit + is Async.Success -> + LaunchedEffect(state.logoutAction) { + onSuccessLogout(state.logoutAction.data) + } + } +} + +// TODO i18n +@Composable +private fun HeaderContent( + state: LogoutState, + modifier: Modifier = Modifier, +) { + val title = when { + state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title) + state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_title) + else -> "Sign out of Element" // TODO + } + val subtitle = when { + state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle) + state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle) + else -> null + } + + val paddingTop = 60.dp + IconTitleSubtitleMolecule( + modifier = modifier.padding(top = paddingTop), + iconResourceId = CommonDrawables.ic_key, + title = title, + subTitle = subtitle, + // iconComposable = iconComposable, + ) +} + +private fun BackupUploadState.isBackingUp(): Boolean { + return when (this) { + BackupUploadState.Unknown, + BackupUploadState.Waiting, + is BackupUploadState.Uploading, + is BackupUploadState.CheckingIfUploadNeeded -> true + BackupUploadState.Done -> false + } +} + +@Composable +private fun BottomMenu( + state: LogoutState, + onLogoutClicked: () -> Unit, + onChangeRecoveryKeyClicked: () -> Unit, +) { + val logoutAction = state.logoutAction + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 20.dp) + ) { + if (state.isLastSession) { + OutlinedButton( + text = stringResource(id = CommonStrings.common_settings), + modifier = Modifier.fillMaxWidth(), + onClick = onChangeRecoveryKeyClicked, + ) + } + val signOutSubmitRes = when { + logoutAction is Async.Loading -> R.string.screen_signout_in_progress_dialog_content + state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway + else -> CommonStrings.action_signout + } + Button( + text = stringResource(id = signOutSubmitRes), + showProgress = logoutAction is Async.Loading, + destructive = true, + modifier = Modifier.fillMaxWidth(), + onClick = onLogoutClicked, + ) + } +} + +@Composable +private fun Content( + state: LogoutState, +) { + if (state.backupUploadState is BackupUploadState.Uploading) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 60.dp, start = 20.dp, end = 20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + progress = state.backupUploadState.backedUpCount.toFloat() / state.backupUploadState.totalCount.toFloat(), + trackColor = ElementTheme.colors.progressIndicatorTrackColor, + ) + Text( + modifier = Modifier.align(Alignment.End), + text = "${state.backupUploadState.backedUpCount} / ${state.backupUploadState.totalCount}", + style = ElementTheme.typography.fontBodySmRegular, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun LogoutViewPreview( + @PreviewParameter(LogoutStateProvider::class) state: LogoutState, +) = ElementPreview { + LogoutView( + state, + onChangeRecoveryKeyClicked = {}, + onSuccessLogout = {} + ) +} diff --git a/features/logout/api/src/main/res/values-cs/translations.xml b/features/logout/impl/src/main/res/values-cs/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-cs/translations.xml rename to features/logout/impl/src/main/res/values-cs/translations.xml diff --git a/features/logout/api/src/main/res/values-de/translations.xml b/features/logout/impl/src/main/res/values-de/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-de/translations.xml rename to features/logout/impl/src/main/res/values-de/translations.xml diff --git a/features/logout/api/src/main/res/values-es/translations.xml b/features/logout/impl/src/main/res/values-es/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-es/translations.xml rename to features/logout/impl/src/main/res/values-es/translations.xml diff --git a/features/logout/api/src/main/res/values-fr/translations.xml b/features/logout/impl/src/main/res/values-fr/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-fr/translations.xml rename to features/logout/impl/src/main/res/values-fr/translations.xml diff --git a/features/logout/api/src/main/res/values-it/translations.xml b/features/logout/impl/src/main/res/values-it/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-it/translations.xml rename to features/logout/impl/src/main/res/values-it/translations.xml diff --git a/features/logout/api/src/main/res/values-ro/translations.xml b/features/logout/impl/src/main/res/values-ro/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-ro/translations.xml rename to features/logout/impl/src/main/res/values-ro/translations.xml diff --git a/features/logout/api/src/main/res/values-ru/translations.xml b/features/logout/impl/src/main/res/values-ru/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-ru/translations.xml rename to features/logout/impl/src/main/res/values-ru/translations.xml diff --git a/features/logout/api/src/main/res/values-sk/translations.xml b/features/logout/impl/src/main/res/values-sk/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-sk/translations.xml rename to features/logout/impl/src/main/res/values-sk/translations.xml diff --git a/features/logout/api/src/main/res/values-zh-rTW/translations.xml b/features/logout/impl/src/main/res/values-zh-rTW/translations.xml similarity index 100% rename from features/logout/api/src/main/res/values-zh-rTW/translations.xml rename to features/logout/impl/src/main/res/values-zh-rTW/translations.xml diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/impl/src/main/res/values/localazy.xml similarity index 100% rename from features/logout/api/src/main/res/values/localazy.xml rename to features/logout/impl/src/main/res/values/localazy.xml diff --git a/features/logout/impl/src/main/res/values/tmp.xml b/features/logout/impl/src/main/res/values/tmp.xml new file mode 100644 index 0000000000..e9e1e376c6 --- /dev/null +++ b/features/logout/impl/src/main/res/values/tmp.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt deleted file mode 100644 index 52e673cba6..0000000000 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt +++ /dev/null @@ -1,87 +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.logout.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.features.logout.api.LogoutPreferenceEvents -import io.element.android.features.logout.api.LogoutPreferenceState -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.matrix.test.A_THROWABLE -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.tests.testutils.WarmUpRule -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class LogoutPreferencePresenterTest { - - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val presenter = DefaultLogoutPreferencePresenter( - FakeMatrixClient(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) - } - } - - @Test - fun `present - logout`() = runTest { - val presenter = DefaultLogoutPreferencePresenter( - FakeMatrixClient(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(LogoutPreferenceEvents.Logout) - val loadingState = awaitItem() - assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) - val successState = awaitItem() - assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) - } - } - - @Test - fun `present - logout with error`() = runTest { - val matrixClient = FakeMatrixClient() - val presenter = DefaultLogoutPreferencePresenter( - matrixClient, - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - matrixClient.givenLogoutError(A_THROWABLE) - initialState.eventSink.invoke(LogoutPreferenceEvents.Logout) - val loadingState = awaitItem() - assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) - val successState = awaitItem() - assertThat(successState.logoutAction).isEqualTo(Async.Failure(A_THROWABLE)) - } - } -} - diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt new file mode 100644 index 0000000000..fe703780ba --- /dev/null +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -0,0 +1,199 @@ +/* + * 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.logout.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.Async +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.encryption.BackupUploadState +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class LogoutPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createLogoutPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isLastSession).isFalse() + assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) + assertThat(initialState.showConfirmationDialog).isFalse() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - initial state - last session`() = runTest { + val presenter = createLogoutPresenter( + encryptionService = FakeEncryptionService().apply { + givenIsLastDevice(true) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isLastSession).isTrue() + assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) + assertThat(initialState.showConfirmationDialog).isFalse() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - initial state - backing up`() = runTest { + val encryptionService = FakeEncryptionService() + val presenter = createLogoutPresenter( + encryptionService = encryptionService + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isLastSession).isFalse() + assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) + assertThat(initialState.showConfirmationDialog).isFalse() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + encryptionService.emitBackupUploadState(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) + val state = awaitItem() + assertThat(state.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) + encryptionService.emitBackupUploadState(BackupUploadState.Done) + val doneState = awaitItem() + assertThat(doneState.backupUploadState).isEqualTo(BackupUploadState.Done) + } + } + + @Test + fun `present - logout then cancel`() = runTest { + val presenter = createLogoutPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + val confirmationState = awaitItem() + assertThat(confirmationState.showConfirmationDialog).isTrue() + initialState.eventSink.invoke(LogoutEvents.CloseDialogs) + val finalState = awaitItem() + assertThat(finalState.showConfirmationDialog).isFalse() + } + } + + @Test + fun `present - logout then confirm`() = runTest { + val presenter = createLogoutPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + val confirmationState = awaitItem() + assertThat(confirmationState.showConfirmationDialog).isTrue() + confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState.showConfirmationDialog).isFalse() + assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) + } + } + + @Test + fun `present - logout with error then cancel`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenLogoutError(A_THROWABLE) + } + val presenter = createLogoutPresenter( + matrixClient, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + val confirmationState = awaitItem() + assertThat(confirmationState.showConfirmationDialog).isTrue() + confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState.showConfirmationDialog).isFalse() + assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) + val errorState = awaitItem() + assertThat(errorState.logoutAction).isEqualTo(Async.Failure(A_THROWABLE)) + errorState.eventSink.invoke(LogoutEvents.CloseDialogs) + val finalState = awaitItem() + assertThat(finalState.logoutAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - logout with error then force`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenLogoutError(A_THROWABLE) + } + val presenter = createLogoutPresenter( + matrixClient, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + val confirmationState = awaitItem() + assertThat(confirmationState.showConfirmationDialog).isTrue() + confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState.showConfirmationDialog).isFalse() + assertThat(loadingState.logoutAction).isInstanceOf(Async.Loading::class.java) + val errorState = awaitItem() + assertThat(errorState.logoutAction).isEqualTo(Async.Failure(A_THROWABLE)) + errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true)) + val loadingState2 = awaitItem() + assertThat(loadingState2.showConfirmationDialog).isFalse() + assertThat(loadingState2.logoutAction).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.logoutAction).isInstanceOf(Async.Success::class.java) + } + } + + private fun createLogoutPresenter( + matrixClient: MatrixClient = FakeMatrixClient(), + encryptionService: EncryptionService = FakeEncryptionService(), + ): LogoutPresenter = LogoutPresenter( + matrixClient = matrixClient, + encryptionService = encryptionService, + ) +} + 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 a0d2b8e057..9d087e254e 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 @@ -47,6 +47,7 @@ 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 e4ec04c263..406d6a2ff4 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.libraries.featureflag.ui) implementation(projects.libraries.network) implementation(projects.libraries.pushstore.api) + implementation(projects.libraries.indicator.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) @@ -78,6 +79,7 @@ dependencies { testImplementation(projects.libraries.pushstore.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) + testImplementation(projects.libraries.indicator.impl) testImplementation(projects.features.logout.impl) testImplementation(projects.services.analytics.test) testImplementation(projects.features.analytics.impl) 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 2b46fa0746..bd7d961123 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 @@ -30,6 +30,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.impl.about.AboutNode import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode @@ -53,6 +54,7 @@ class PreferencesFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, private val lockScreenEntryPoint: LockScreenEntryPoint, + private val logoutEntryPoint: LogoutEntryPoint, ) : BackstackNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), @@ -92,6 +94,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data class UserProfile(val matrixUser: MatrixUser) : NavTarget + + @Parcelize + data object SignOut : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -106,6 +111,10 @@ class PreferencesFlowNode @AssistedInject constructor( plugins().forEach { it.onVerifyClicked() } } + override fun onSecureBackupClicked() { + plugins().forEach { it.onSecureBackupClicked() } + } + override fun onOpenAnalytics() { backstack.push(NavTarget.AnalyticsSettings) } @@ -133,6 +142,10 @@ class PreferencesFlowNode @AssistedInject constructor( override fun onOpenUserProfile(matrixUser: MatrixUser) { backstack.push(NavTarget.UserProfile(matrixUser)) } + + override fun onSignOutClicked() { + backstack.push(NavTarget.SignOut) + } } createNode(buildContext, plugins = listOf(callback)) } @@ -182,6 +195,16 @@ class PreferencesFlowNode @AssistedInject constructor( .target(LockScreenEntryPoint.Target.Settings) .build() } + NavTarget.SignOut -> { + val callBack: LogoutEntryPoint.Callback = object : LogoutEntryPoint.Callback { + override fun onChangeRecoveryKeyClicked() { + plugins().forEach { it.onSecureBackupClicked() } + } + } + logoutEntryPoint.nodeBuilder(this, buildContext) + .callback(callBack) + .build() + } } } 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 7ea1fa2e8a..43ceb427b9 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 @@ -31,7 +31,6 @@ import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTa import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.theme.ElementTheme -import timber.log.Timber @ContributesNode(SessionScope::class) class PreferencesRootNode @AssistedInject constructor( @@ -43,6 +42,7 @@ class PreferencesRootNode @AssistedInject constructor( interface Callback : Plugin { fun onOpenBugReport() fun onVerifyClicked() + fun onSecureBackupClicked() fun onOpenAnalytics() fun onOpenAbout() fun onOpenDeveloperSettings() @@ -50,6 +50,7 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenLockScreenSettings() fun onOpenAdvancedSettings() fun onOpenUserProfile(matrixUser: MatrixUser) + fun onSignOutClicked() } private fun onOpenBugReport() { @@ -60,6 +61,10 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onVerifyClicked() } } + private fun onSecureBackupClicked() { + plugins().forEach { it.onSecureBackupClicked() } + } + private fun onOpenDeveloperSettings() { plugins().forEach { it.onOpenDeveloperSettings() } } @@ -102,6 +107,10 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenUserProfile(matrixUser) } } + private fun onSignOutClicked() { + plugins().forEach { it.onSignOutClicked() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -115,20 +124,14 @@ class PreferencesRootNode @AssistedInject constructor( onOpenAnalytics = this::onOpenAnalytics, onOpenAbout = this::onOpenAbout, onVerifyClicked = this::onVerifyClicked, + onSecureBackupClicked = this::onSecureBackupClicked, onOpenDeveloperSettings = this::onOpenDeveloperSettings, onOpenAdvancedSettings = this::onOpenAdvancedSettings, - onSuccessLogout = { onSuccessLogout(activity, it) }, onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) }, onOpenNotificationSettings = this::onOpenNotificationSettings, onOpenLockScreenSettings = this::onOpenLockScreenSettings, onOpenUserProfile = this::onOpenUserProfile, + onSignOutClicked = this::onSignOutClicked, ) } - - private fun onSuccessLogout(activity: Activity, url: String?) { - Timber.d("Success logout with result url: $url") - url?.let { - activity.openUrlInChromeCustomTab(null, false, it) - } - } } 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 64132db982..32e85ffb35 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 @@ -24,13 +24,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.features.logout.api.LogoutPreferencePresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.user.MatrixUser @@ -42,7 +42,6 @@ import kotlinx.coroutines.launch import javax.inject.Inject class PreferencesRootPresenter @Inject constructor( - private val logoutPresenter: LogoutPreferencePresenter, private val matrixClient: MatrixClient, private val sessionVerificationService: SessionVerificationService, private val analyticsService: AnalyticsService, @@ -50,6 +49,7 @@ class PreferencesRootPresenter @Inject constructor( private val versionFormatter: VersionFormatter, private val snackbarDispatcher: SnackbarDispatcher, private val featureFlagService: FeatureFlagService, + private val indicatorService: IndicatorService, ) : Presenter { @Composable @@ -76,6 +76,8 @@ 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 showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator() + val accountManagementUrl: MutableState = remember { mutableStateOf(null) } @@ -87,13 +89,13 @@ class PreferencesRootPresenter @Inject constructor( initAccountManagementUrl(accountManagementUrl, devicesManagementUrl) } - val logoutState = logoutPresenter.present() val showDeveloperSettings = buildType != BuildType.RELEASE return PreferencesRootState( - logoutState = logoutState, myUser = matrixUser.value, version = versionFormatter.get(), showCompleteVerification = showCompleteVerification, + showSecureBackup = !showCompleteVerification, + showSecureBackupBadge = showSecureBackupIndicator, accountManagementUrl = accountManagementUrl.value, devicesManagementUrl = devicesManagementUrl.value, showAnalyticsSettings = hasAnalyticsProviders, 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 d6ff4855de..fec09f150f 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 @@ -16,15 +16,15 @@ package io.element.android.features.preferences.impl.root -import io.element.android.features.logout.api.LogoutPreferenceState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.user.MatrixUser data class PreferencesRootState( - val logoutState: LogoutPreferenceState, val myUser: MatrixUser?, val version: String, val showCompleteVerification: Boolean, + val showSecureBackup: Boolean, + val showSecureBackupBadge: Boolean, val accountManagementUrl: String?, val devicesManagementUrl: String?, val showAnalyticsSettings: Boolean, 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 07dd6240bf..74d8b0f0c9 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 @@ -16,15 +16,15 @@ package io.element.android.features.preferences.impl.root -import io.element.android.features.logout.api.aLogoutPreferenceState import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.ui.strings.CommonStrings fun aPreferencesRootState() = PreferencesRootState( - logoutState = aLogoutPreferenceState(), myUser = null, version = "Version 1.1 (1)", showCompleteVerification = true, + showSecureBackup = true, + showSecureBackupBadge = true, accountManagementUrl = "aUrl", devicesManagementUrl = "anOtherUrl", showAnalyticsSettings = true, 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 34a4890348..e25428f724 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 @@ -29,10 +29,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.logout.api.LogoutPreferenceView import io.element.android.features.preferences.impl.user.UserPreferences -import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight @@ -51,6 +50,7 @@ fun PreferencesRootView( state: PreferencesRootState, onBackPressed: () -> Unit, onVerifyClicked: () -> Unit, + onSecureBackupClicked: () -> Unit, onManageAccountClicked: (url: String) -> Unit, onOpenAnalytics: () -> Unit, onOpenRageShake: () -> Unit, @@ -58,9 +58,9 @@ fun PreferencesRootView( onOpenAbout: () -> Unit, onOpenDeveloperSettings: () -> Unit, onOpenAdvancedSettings: () -> Unit, - onSuccessLogout: (logoutUrlResult: String?) -> Unit, onOpenNotificationSettings: () -> Unit, onOpenUserProfile: (MatrixUser) -> Unit, + onSignOutClicked: () -> Unit, modifier: Modifier = Modifier, ) { val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) @@ -84,6 +84,16 @@ fun PreferencesRootView( icon = Icons.Outlined.VerifiedUser, onClick = onVerifyClicked, ) + } + if (state.showSecureBackup) { + PreferenceText( + title = stringResource(id = CommonStrings.common_chat_backup), + iconResourceId = CommonDrawables.ic_key_filled, + showEndBadge = state.showSecureBackupBadge, + onClick = onSecureBackupClicked, + ) + } + if (state.showCompleteVerification || state.showSecureBackup) { HorizontalDivider() } if (state.accountManagementUrl != null) { @@ -143,9 +153,10 @@ fun PreferencesRootView( DeveloperPreferencesView(onOpenDeveloperSettings) } HorizontalDivider() - LogoutPreferenceView( - state = state.logoutState, - onSuccessLogout = onSuccessLogout, + PreferenceText( + title = stringResource(id = CommonStrings.action_signout), + iconResourceId = CommonDrawables.ic_compound_leave, + onClick = onSignOutClicked, ) Text( modifier = Modifier @@ -189,10 +200,11 @@ private fun ContentToPreview(matrixUser: MatrixUser) { onOpenAdvancedSettings = {}, onOpenAbout = {}, onVerifyClicked = {}, - onSuccessLogout = {}, + onSecureBackupClicked = {}, onManageAccountClicked = {}, onOpenNotificationSettings = {}, onOpenLockScreenSettings = {}, onOpenUserProfile = {}, + onSignOutClicked = {}, ) } 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 e9eaf5ef13..c9ef8b5560 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 @@ -20,15 +20,15 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.logout.impl.DefaultLogoutPreferencePresenter -import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_AVATAR_URL 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.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule @@ -44,16 +44,19 @@ class PreferencesRootPresenterTest { @Test fun `present - initial state`() = runTest { val matrixClient = FakeMatrixClient() - val logoutPresenter = DefaultLogoutPreferencePresenter(matrixClient) + val sessionVerificationService = FakeSessionVerificationService() val presenter = PreferencesRootPresenter( - logoutPresenter, matrixClient, - FakeSessionVerificationService(), + sessionVerificationService, FakeAnalyticsService(), BuildType.DEBUG, FakeVersionFormatter(), SnackbarDispatcher(), - FakeFeatureFlagService() + FakeFeatureFlagService(), + DefaultIndicatorService( + sessionVerificationService = sessionVerificationService, + encryptionService = FakeEncryptionService(), + ), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -62,7 +65,6 @@ class PreferencesRootPresenterTest { assertThat(initialState.myUser).isNull() assertThat(initialState.version).isEqualTo("A Version") val loadedState = awaitItem() - assertThat(loadedState.logoutState.logoutAction).isEqualTo(Async.Uninitialized) assertThat(loadedState.myUser).isEqualTo( MatrixUser( userId = matrixClient.sessionId, diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index 17817ebc91..5b52a05c2e 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.eventformatter.api) + implementation(projects.libraries.indicator.api) implementation(projects.libraries.deeplink) implementation(projects.features.invitelist.api) implementation(projects.features.networkmonitor.api) @@ -65,6 +66,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.dateformatter.test) testImplementation(projects.libraries.eventformatter.test) + testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.permissions.noop) testImplementation(projects.features.invitelist.test) testImplementation(projects.features.networkmonitor.test) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt index e377764942..d54bc604cf 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt @@ -23,6 +23,7 @@ sealed interface RoomListEvents { data class UpdateFilter(val newFilter: String) : RoomListEvents data class UpdateVisibleRange(val range: IntRange) : RoomListEvents data object DismissRequestVerificationPrompt : RoomListEvents + data object DismissRecoveryKeyPrompt : RoomListEvents data object ToggleSearchResults : RoomListEvents data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents data object HideContextMenu : RoomListEvents 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 5238144755..4a1e10d318 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 @@ -35,7 +35,10 @@ import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState +import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.MatrixClient +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.user.MatrixUser import io.element.android.libraries.matrix.api.user.getCurrentUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -53,6 +56,8 @@ class RoomListPresenter @Inject constructor( private val inviteStateDataSource: InviteStateDataSource, private val leaveRoomPresenter: LeaveRoomPresenter, private val roomListDataSource: RoomListDataSource, + private val encryptionService: EncryptionService, + private val indicatorService: IndicatorService, ) : Presenter { @Composable @@ -78,6 +83,16 @@ class RoomListPresenter @Inject constructor( val displayVerificationPrompt by remember { derivedStateOf { canVerifySession && !verificationPromptDismissed } } + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + var recoveryKeyPromptDismissed by rememberSaveable { mutableStateOf(false) } + val displayRecoveryKeyPrompt by remember { + derivedStateOf { + recoveryState == RecoveryState.INCOMPLETE && !recoveryKeyPromptDismissed + } + } + + // Avatar indicator + val showAvatarIndicator by indicatorService.showRoomListTopBarIndicator() var displaySearchResults by rememberSaveable { mutableStateOf(false) } @@ -88,6 +103,7 @@ class RoomListPresenter @Inject constructor( is RoomListEvents.UpdateFilter -> roomListDataSource.updateFilter(event.newFilter) is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true + RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true RoomListEvents.ToggleSearchResults -> { if (displaySearchResults) { roomListDataSource.updateFilter("") @@ -109,10 +125,12 @@ class RoomListPresenter @Inject constructor( return RoomListState( matrixUser = matrixUser.value, + showAvatarIndicator = showAvatarIndicator, roomList = roomList, filter = filter, filteredRoomList = filteredRoomList, displayVerificationPrompt = displayVerificationPrompt, + displayRecoveryKeyPrompt = displayRecoveryKeyPrompt, snackbarMessage = snackbarMessage, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, invitesState = inviteStateDataSource.inviteState(), 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 f0dfe8e3a9..b0c87b88aa 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 @@ -27,10 +27,12 @@ import kotlinx.collections.immutable.ImmutableList @Immutable data class RoomListState( val matrixUser: MatrixUser?, + val showAvatarIndicator: Boolean, val roomList: ImmutableList, val filter: String?, val filteredRoomList: ImmutableList, val displayVerificationPrompt: Boolean, + val displayRecoveryKeyPrompt: Boolean, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val invitesState: InvitesState, 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 7199bfd227..6610136f5d 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 @@ -41,20 +41,25 @@ open class RoomListStateProvider : PreviewParameterProvider { aRoomListState().copy(invitesState = InvitesState.NewInvites), aRoomListState().copy(displaySearchResults = true, filter = "", filteredRoomList = persistentListOf()), aRoomListState().copy(displaySearchResults = true), - aRoomListState().copy(contextMenu = RoomListState.ContextMenu.Shown( - roomId = RoomId("!aRoom:aDomain"), roomName = "A nice room name" - )) + aRoomListState().copy( + contextMenu = RoomListState.ContextMenu.Shown( + roomId = RoomId("!aRoom:aDomain"), roomName = "A nice room name" + ) + ), + aRoomListState().copy(displayRecoveryKeyPrompt = true), ) } internal fun aRoomListState() = RoomListState( matrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), + showAvatarIndicator = false, roomList = aRoomListRoomSummaryList(), filter = "filter", filteredRoomList = aRoomListRoomSummaryList(), hasNetworkConnection = true, snackbarMessage = null, displayVerificationPrompt = false, + displayRecoveryKeyPrompt = false, invitesState = InvitesState.NoInvites, displaySearchResults = false, contextMenu = RoomListState.ContextMenu.Hidden, 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 b01c5fba4d..2d7ea1e4b1 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 @@ -45,6 +45,7 @@ import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import io.element.android.features.leaveroom.api.LeaveRoomView import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer +import io.element.android.features.roomlist.impl.components.ConfirmRecoveryKeyBanner import io.element.android.features.roomlist.impl.components.RequestVerificationHeader import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.components.RoomListTopBar @@ -172,6 +173,7 @@ private fun RoomListContent( topBar = { RoomListTopBar( matrixUser = state.matrixUser, + showAvatarIndicator = state.showAvatarIndicator, areSearchResultsDisplayed = state.displaySearchResults, onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) }, onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) }, @@ -195,6 +197,13 @@ private fun RoomListContent( onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } ) } + } else if (state.displayRecoveryKeyPrompt) { + item { + ConfirmRecoveryKeyBanner( + onContinueClicked = onOpenSettings, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } + ) + } } if (state.invitesState != InvitesState.NoInvites) { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/ConfirmRecoveryKeyBanner.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/ConfirmRecoveryKeyBanner.kt new file mode 100644 index 0000000000..71f9c78530 --- /dev/null +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/ConfirmRecoveryKeyBanner.kt @@ -0,0 +1,49 @@ +/* + * 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 ConfirmRecoveryKeyBanner( + onContinueClicked: () -> Unit, + onDismissClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + DialogLikeBannerMolecule( + modifier = modifier, + title = stringResource(R.string.confirm_recovery_key_banner_title), + content = stringResource(R.string.confirm_recovery_key_banner_message), + onSubmitClicked = onContinueClicked, + onDismissClicked = onDismissClicked, + ) +} + +@PreviewsDayNight +@Composable +internal fun ConfirmRecoveryKeyBannerPreview() = ElementPreview { + ConfirmRecoveryKeyBanner( + onContinueClicked = {}, + onDismissClicked = {}, + ) +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index 6c2f07bbac..57bfccabb7 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -20,6 +20,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api @@ -46,11 +47,12 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import io.element.android.features.roomlist.impl.R +import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatarBloom -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleDown import io.element.android.libraries.designsystem.text.roundToPx import io.element.android.libraries.designsystem.text.toDp @@ -79,6 +81,7 @@ private val avatarBloomSize = 430.dp @Composable fun RoomListTopBar( matrixUser: MatrixUser?, + showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, onFilterChanged: (String) -> Unit, onToggleSearch: () -> Unit, @@ -103,6 +106,7 @@ fun RoomListTopBar( DefaultRoomListTopBar( matrixUser = matrixUser, + showAvatarIndicator = showAvatarIndicator, areSearchResultsDisplayed = areSearchResultsDisplayed, onOpenSettings = onOpenSettings, onSearchClicked = onToggleSearch, @@ -116,6 +120,7 @@ fun RoomListTopBar( @Composable private fun DefaultRoomListTopBar( matrixUser: MatrixUser?, + showAvatarIndicator: Boolean, areSearchResultsDisplayed: Boolean, scrollBehavior: TopAppBarScrollBehavior, onOpenSettings: () -> Unit, @@ -198,6 +203,13 @@ private fun DefaultRoomListTopBar( avatarData = it, contentDescription = stringResource(CommonStrings.common_settings), ) + if (showAvatarIndicator) { + RedIndicatorAtom( + modifier = Modifier + .padding(4.5.dp) + .align(Alignment.TopEnd) + ) + } } } }, @@ -273,6 +285,22 @@ private fun DefaultRoomListTopBar( internal fun DefaultRoomListTopBarPreview() = ElementPreview { DefaultRoomListTopBar( matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), + showAvatarIndicator = false, + areSearchResultsDisplayed = false, + scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), + onOpenSettings = {}, + onSearchClicked = {}, + onMenuActionClicked = {}, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@PreviewsDayNight +@Composable +internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview { + DefaultRoomListTopBar( + matrixUser = MatrixUser(UserId("@id:domain"), "Alice"), + showAvatarIndicator = true, areSearchResultsDisplayed = false, scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()), onOpenSettings = {}, diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index 26b0bbee9e..08a630c6b5 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -1,5 +1,7 @@ + "Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup." + "Confirm your recovery key" "Create a new conversation or room" "Get started by messaging someone." "No chats yet." 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 fd5c0160fc..66e0548215 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 @@ -37,7 +37,10 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter +import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient +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.RoomNotificationMode import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus @@ -48,6 +51,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_ID 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.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService @@ -83,6 +87,32 @@ class RoomListPresenterTests { Truth.assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID) Truth.assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME) Truth.assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL) + Truth.assertThat(withUserState.showAvatarIndicator).isFalse() + scope.cancel() + } + } + + @Test + fun `present - show avatar indicator`() = runTest { + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + val encryptionService = FakeEncryptionService() + val sessionVerificationService = FakeSessionVerificationService() + val presenter = createRoomListPresenter( + encryptionService = encryptionService, + sessionVerificationService = sessionVerificationService, + coroutineScope = scope + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + Truth.assertThat(initialState.showAvatarIndicator).isFalse() + sessionVerificationService.givenCanVerifySession(false) + Truth.assertThat(awaitItem().showAvatarIndicator).isFalse() + encryptionService.emitBackupState(BackupState.UNKNOWN) + val finalState = awaitItem() + Truth.assertThat(finalState.showAvatarIndicator).isTrue() scope.cancel() } } @@ -131,7 +161,7 @@ class RoomListPresenterTests { roomListService = roomListService ) val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -341,7 +371,7 @@ class RoomListPresenterTests { notificationSettingsService = notificationSettingsService ) val scope = CoroutineScope(coroutineContext + SupervisorJob()) - val presenter = createRoomListPresenter(client = matrixClient , coroutineScope = scope) + val presenter = createRoomListPresenter(client = matrixClient, coroutineScope = scope) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -369,7 +399,8 @@ class RoomListPresenterTests { givenFormat(A_FORMATTED_DATE) }, roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), - coroutineScope: CoroutineScope = this, + encryptionService: EncryptionService = FakeEncryptionService(), + coroutineScope: CoroutineScope, ) = RoomListPresenter( client = client, sessionVerificationService = sessionVerificationService, @@ -384,7 +415,12 @@ class RoomListPresenterTests { coroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService = client.notificationSettingsService(), appScope = coroutineScope - ) + ), + encryptionService = encryptionService, + indicatorService = DefaultIndicatorService( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, + ), ) } diff --git a/features/securebackup/api/build.gradle.kts b/features/securebackup/api/build.gradle.kts new file mode 100644 index 0000000000..c9117d1d39 --- /dev/null +++ b/features/securebackup/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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.features.securebackup.api" +} + +dependencies { + implementation(projects.libraries.architecture) +} diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferencePresenter.kt b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt similarity index 76% rename from features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferencePresenter.kt rename to features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt index b42744f912..8824fdf84b 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferencePresenter.kt +++ b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt @@ -14,8 +14,8 @@ * limitations under the License. */ -package io.element.android.features.logout.api +package io.element.android.features.securebackup.api -import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.SimpleFeatureEntryPoint -interface LogoutPreferencePresenter : Presenter +interface SecureBackupEntryPoint : SimpleFeatureEntryPoint diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts new file mode 100644 index 0000000000..68e8dc066f --- /dev/null +++ b/features/securebackup/impl/build.gradle.kts @@ -0,0 +1,56 @@ +/* + * 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.ksp) + alias(libs.plugins.anvil) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.securebackup.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(projects.anvilannotations) + + implementation(projects.libraries.androidutils) + implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + api(libs.statemachine) + api(projects.features.securebackup.api) + + testImplementation(libs.test.junit) + 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/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 new file mode 100644 index 0000000000..a870255a9a --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt @@ -0,0 +1,32 @@ +/* + * 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.securebackup.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.securebackup.api.SecureBackupEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultSecureBackupEntryPoint @Inject constructor() : SecureBackupEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/LoggerTag.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/LoggerTag.kt new file mode 100644 index 0000000000..b8b56f5ace --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/LoggerTag.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.features.securebackup.impl + +import io.element.android.libraries.core.log.logger.LoggerTag + +private val loggerTag = LoggerTag("SecureBackup") +val loggerTagRoot = LoggerTag("Root", loggerTag) +val loggerTagSetup = LoggerTag("Setup", loggerTag) +val loggerTagDisable = LoggerTag("Disable", loggerTag) + diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupConfig.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupConfig.kt new file mode 100644 index 0000000000..045a320e4a --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupConfig.kt @@ -0,0 +1,22 @@ +/* + * 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.securebackup.impl + +// TODO Move to appconfig module when it will be available +object SecureBackupConfig { + const val LearnMoreUrl: String = "https://element.io/help#encryption5" +} 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 new file mode 100644 index 0000000000..0ce510336c --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt @@ -0,0 +1,133 @@ +/* + * 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.securebackup.impl + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +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 +import io.element.android.features.securebackup.impl.root.SecureBackupRootNode +import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode +import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.SessionScope +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class SecureBackupFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Root : NavTarget + + @Parcelize + data object Setup : NavTarget + + @Parcelize + data object Change : NavTarget + + @Parcelize + data object Disable : NavTarget + + @Parcelize + data object Enable : NavTarget + + @Parcelize + data object EnterRecoveryKey : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Root -> { + val callback = object : SecureBackupRootNode.Callback { + override fun onSetupClicked() { + backstack.push(NavTarget.Setup) + } + + override fun onChangeClicked() { + backstack.push(NavTarget.Change) + } + + override fun onDisableClicked() { + backstack.push(NavTarget.Disable) + } + + override fun onEnableClicked() { + backstack.push(NavTarget.Enable) + } + + override fun onConfirmRecoveryKeyClicked() { + backstack.push(NavTarget.EnterRecoveryKey) + } + } + createNode(buildContext, listOf(callback)) + } + NavTarget.Setup -> { + val inputs = SecureBackupSetupNode.Inputs( + isChangeRecoveryKeyUserStory = false, + ) + createNode(buildContext, listOf(inputs)) + } + NavTarget.Change -> { + val inputs = SecureBackupSetupNode.Inputs( + isChangeRecoveryKeyUserStory = true, + ) + createNode(buildContext, listOf(inputs)) + } + NavTarget.Disable -> { + createNode(buildContext) + } + NavTarget.Enable -> { + createNode(buildContext) + } + NavTarget.EnterRecoveryKey -> { + createNode(buildContext) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler() + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvents.kt new file mode 100644 index 0000000000..4cda13f7af --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvents.kt @@ -0,0 +1,22 @@ +/* + * 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.securebackup.impl.disable + +sealed interface SecureBackupDisableEvents { + data class DisableBackup(val force: Boolean) : SecureBackupDisableEvents + data object DismissDialogs : SecureBackupDisableEvents +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt new file mode 100644 index 0000000000..42f1c81f57 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt @@ -0,0 +1,45 @@ +/* + * 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.securebackup.impl.disable + +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 SecureBackupDisableNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: SecureBackupDisablePresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + SecureBackupDisableView( + state = state, + modifier = modifier, + onDone = ::navigateUp, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt new file mode 100644 index 0000000000..60b4691661 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt @@ -0,0 +1,80 @@ +/* + * 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.securebackup.impl.disable + +import androidx.compose.runtime.Composable +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.setValue +import io.element.android.features.securebackup.impl.loggerTagDisable +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class SecureBackupDisablePresenter @Inject constructor( + private val encryptionService: EncryptionService, + private val buildMeta: BuildMeta, +) : Presenter { + + @Composable + override fun present(): SecureBackupDisableState { + val backupState by encryptionService.backupStateStateFlow.collectAsState() + Timber.tag(loggerTagDisable.value).d("backupState: $backupState") + val disableAction = remember { mutableStateOf>(Async.Uninitialized) } + val coroutineScope = rememberCoroutineScope() + var showDialog by remember { mutableStateOf(false) } + fun handleEvents(event: SecureBackupDisableEvents) { + when (event) { + is SecureBackupDisableEvents.DisableBackup -> if (event.force) { + showDialog = false + coroutineScope.disableBackup(disableAction) + } else { + showDialog = true + } + SecureBackupDisableEvents.DismissDialogs -> { + showDialog = false + disableAction.value = Async.Uninitialized + } + } + } + + return SecureBackupDisableState( + backupState = backupState, + disableAction = disableAction.value, + showConfirmationDialog = showDialog, + appName = buildMeta.applicationName, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.disableBackup(disableAction: MutableState>) = launch { + suspend { + Timber.tag(loggerTagDisable.value).d("Calling encryptionService.disableRecovery()") + encryptionService.disableRecovery().getOrThrow() + }.runCatchingUpdatingState(disableAction) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableState.kt new file mode 100644 index 0000000000..41e39c398e --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableState.kt @@ -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. + */ + +package io.element.android.features.securebackup.impl.disable + +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.encryption.BackupState + +data class SecureBackupDisableState( + val backupState: BackupState, + val disableAction: Async, + val showConfirmationDialog: Boolean, + val appName: String, + val eventSink: (SecureBackupDisableEvents) -> Unit +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStateProvider.kt new file mode 100644 index 0000000000..8c22e36968 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStateProvider.kt @@ -0,0 +1,44 @@ +/* + * 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.securebackup.impl.disable + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.encryption.BackupState + +open class SecureBackupDisableStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSecureBackupDisableState(), + aSecureBackupDisableState(showConfirmationDialog = true), + aSecureBackupDisableState(disableAction = Async.Loading()), + aSecureBackupDisableState(disableAction = Async.Failure(Exception("Failed to disable"))), + // Add other states here + ) +} + +fun aSecureBackupDisableState( + backupState: BackupState = BackupState.UNKNOWN, + disableAction: Async = Async.Uninitialized, + showConfirmationDialog: Boolean = false, +) = SecureBackupDisableState( + backupState = backupState, + disableAction = disableAction, + showConfirmationDialog = showConfirmationDialog, + appName = "Element", + eventSink = {} +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt new file mode 100644 index 0000000000..290086849f --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt @@ -0,0 +1,162 @@ +/* + * 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.securebackup.impl.disable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.securebackup.impl.R +import io.element.android.libraries.architecture.Async +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.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +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.Text +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun SecureBackupDisableView( + state: SecureBackupDisableState, + onDone: () -> Unit, + modifier: Modifier = Modifier, +) { + LaunchedEffect(state.disableAction) { + if (state.disableAction is Async.Success) { + onDone() + } + } + HeaderFooterPage( + modifier = modifier, + header = { + HeaderContent() + }, + footer = { + BottomMenu(state = state) + } + ) { + Content(state = state) + } + if (state.showConfirmationDialog) { + SecureBackupDisableConfirmationDialog( + onConfirm = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = true)) }, + onDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) }, + ) + } else if (state.disableAction is Async.Failure) { + ErrorDialog( + content = state.disableAction.error.let { it.message ?: it.toString() }, + onDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) }, + ) + } +} + +@Composable +private fun SecureBackupDisableConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_key_backup_disable_confirmation_title), + content = stringResource(id = R.string.screen_key_backup_disable_confirmation_description), + submitText = stringResource(id = R.string.screen_key_backup_disable_confirmation_action_turn_off), + destructiveSubmit = true, + onSubmitClicked = onConfirm, + onDismiss = onDismiss, + ) +} + +@Composable +private fun HeaderContent( + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier.padding(top = 60.dp), + iconResourceId = CommonDrawables.ic_key_off, + title = stringResource(id = R.string.screen_key_backup_disable_title), + subTitle = stringResource(id = R.string.screen_key_backup_disable_description), + ) +} + +@Composable +private fun BottomMenu( + state: SecureBackupDisableState, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 20.dp) + ) { + Button( + text = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable), + showProgress = state.disableAction.isLoading(), + destructive = true, + modifier = Modifier.fillMaxWidth(), + onClick = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = false)) } + ) + } +} + +@Composable +private fun Content(state: SecureBackupDisableState) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 18.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + SecureBackupDisableItem(stringResource(id = R.string.screen_key_backup_disable_description_point_1)) + SecureBackupDisableItem(stringResource(id = R.string.screen_key_backup_disable_description_point_2, state.appName)) + } +} + +@Composable +private fun SecureBackupDisableItem(text: String) { + Row(modifier = Modifier.fillMaxWidth()) { + Icon( + resourceId = CommonDrawables.ic_compound_close, + contentDescription = null, + tint = ElementTheme.colors.iconCriticalPrimary, + modifier = Modifier.size(20.dp) + ) + Text( + modifier = Modifier.padding(start = 8.dp, end = 4.dp), + text = text, + color = ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodyMdRegular, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun SecureBackupDisableViewPreview( + @PreviewParameter(SecureBackupDisableStateProvider::class) state: SecureBackupDisableState +) = ElementPreview { + SecureBackupDisableView( + state = state, + onDone = {}, + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableEvents.kt new file mode 100644 index 0000000000..2695e875f9 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableEvents.kt @@ -0,0 +1,22 @@ +/* + * 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.securebackup.impl.enable + +sealed interface SecureBackupEnableEvents { + data object EnableBackup : SecureBackupEnableEvents + data object DismissDialog : SecureBackupEnableEvents +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt new file mode 100644 index 0000000000..34d03881d6 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt @@ -0,0 +1,45 @@ +/* + * 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.securebackup.impl.enable + +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 SecureBackupEnableNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: SecureBackupEnablePresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + SecureBackupEnableView( + state = state, + modifier = modifier, + onDone = ::navigateUp, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenter.kt new file mode 100644 index 0000000000..dfceb16dea --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenter.kt @@ -0,0 +1,64 @@ +/* + * 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.securebackup.impl.enable + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.features.securebackup.impl.loggerTagDisable +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class SecureBackupEnablePresenter @Inject constructor( + private val encryptionService: EncryptionService, +) : Presenter { + + @Composable + override fun present(): SecureBackupEnableState { + val enableAction = remember { mutableStateOf>(Async.Uninitialized) } + val coroutineScope = rememberCoroutineScope() + fun handleEvents(event: SecureBackupEnableEvents) { + when (event) { + is SecureBackupEnableEvents.EnableBackup -> + coroutineScope.enableBackup(enableAction) + SecureBackupEnableEvents.DismissDialog -> { + enableAction.value = Async.Uninitialized + } + } + } + + return SecureBackupEnableState( + enableAction = enableAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.enableBackup(action: MutableState>) = launch { + suspend { + Timber.tag(loggerTagDisable.value).d("Calling encryptionService.enableBackups()") + encryptionService.enableBackups().getOrThrow() + }.runCatchingUpdatingState(action) + } +} diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableState.kt similarity index 73% rename from features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceState.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableState.kt index 95550ef4c8..7c3aaf839b 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceState.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableState.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.logout.api +package io.element.android.features.securebackup.impl.enable import io.element.android.libraries.architecture.Async -data class LogoutPreferenceState( - val logoutAction: Async, - val eventSink: (LogoutPreferenceEvents) -> Unit, +data class SecureBackupEnableState( + val enableAction: Async, + val eventSink: (SecureBackupEnableEvents) -> Unit ) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableStateProvider.kt new file mode 100644 index 0000000000..494c45b5b4 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableStateProvider.kt @@ -0,0 +1,37 @@ +/* + * 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.securebackup.impl.enable + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async + +open class SecureBackupEnableStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSecureBackupEnableState(), + aSecureBackupEnableState(enableAction = Async.Loading()), + aSecureBackupEnableState(enableAction = Async.Failure(Exception("Failed to enable"))), + // Add other states here + ) +} + +fun aSecureBackupEnableState( + enableAction: Async = Async.Uninitialized, +) = SecureBackupEnableState( + enableAction = enableAction, + eventSink = {} +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt new file mode 100644 index 0000000000..feaeb01d02 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt @@ -0,0 +1,103 @@ +/* + * 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.securebackup.impl.enable + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.securebackup.impl.R +import io.element.android.libraries.architecture.Async +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.dialogs.ErrorDialog +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.utils.CommonDrawables + +@Composable +fun SecureBackupEnableView( + state: SecureBackupEnableState, + onDone: () -> Unit, + modifier: Modifier = Modifier, +) { + LaunchedEffect(state.enableAction) { + if (state.enableAction is Async.Success) { + onDone() + } + } + HeaderFooterPage( + modifier = modifier, + header = { + HeaderContent() + }, + footer = { + BottomMenu(state = state) + } + ) + if (state.enableAction is Async.Failure) { + ErrorDialog( + content = state.enableAction.error.let { it.message ?: it.toString() }, + onDismiss = { state.eventSink.invoke(SecureBackupEnableEvents.DismissDialog) }, + ) + } +} + +@Composable +private fun HeaderContent( + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier.padding(top = 60.dp), + iconResourceId = CommonDrawables.ic_key, + title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable), + subTitle = null, + ) +} + +@Composable +private fun BottomMenu( + state: SecureBackupEnableState, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 20.dp) + ) { + Button( + text = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable), + showProgress = state.enableAction.isLoading(), + modifier = Modifier.fillMaxWidth(), + onClick = { state.eventSink.invoke(SecureBackupEnableEvents.EnableBackup) } + ) + } +} + +@PreviewsDayNight +@Composable +internal fun SecureBackupEnableViewPreview( + @PreviewParameter(SecureBackupEnableStateProvider::class) state: SecureBackupEnableState +) = ElementPreview { + SecureBackupEnableView( + state = state, + onDone = {}, + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvents.kt new file mode 100644 index 0000000000..d0d6eccc7d --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvents.kt @@ -0,0 +1,23 @@ +/* + * 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.securebackup.impl.enter + +sealed interface SecureBackupEnterRecoveryKeyEvents { + data class OnRecoveryKeyChange(val recoveryKey: String) : SecureBackupEnterRecoveryKeyEvents + data object Submit : SecureBackupEnterRecoveryKeyEvents + data object ClearDialog : SecureBackupEnterRecoveryKeyEvents +} 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 new file mode 100644 index 0000000000..dcea7dfdb0 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyNode.kt @@ -0,0 +1,64 @@ +/* + * 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.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 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) { + + @Composable + override fun View(modifier: Modifier) { + val coroutineScope = rememberCoroutineScope() + val state = presenter.present() + SecureBackupEnterRecoveryKeyView( + state = state, + modifier = modifier, + onDone = { + coroutineScope.postSuccessSnackbar() + 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/SecureBackupEnterRecoveryKeyPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt new file mode 100644 index 0000000000..fd32c0066d --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt @@ -0,0 +1,86 @@ +/* + * 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.securebackup.impl.enter + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +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.securebackup.impl.setup.views.RecoveryKeyUserStory +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class SecureBackupEnterRecoveryKeyPresenter @Inject constructor( + private val encryptionService: EncryptionService, +) : Presenter { + + @Composable + override fun present(): SecureBackupEnterRecoveryKeyState { + val coroutineScope = rememberCoroutineScope() + var recoveryKey by rememberSaveable { + mutableStateOf("") + } + val submitAction = remember { + mutableStateOf>(Async.Uninitialized) + } + + fun handleEvents(event: SecureBackupEnterRecoveryKeyEvents) { + when (event) { + SecureBackupEnterRecoveryKeyEvents.ClearDialog -> { + submitAction.value = Async.Uninitialized + } + is SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange -> { + recoveryKey = event.recoveryKey.replace("\\s+".toRegex(), "") + } + SecureBackupEnterRecoveryKeyEvents.Submit -> { + // No need to remove the spaces, the SDK will do it. + coroutineScope.submitRecoveryKey(recoveryKey, submitAction) + } + } + } + + return SecureBackupEnterRecoveryKeyState( + recoveryKeyViewState = RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Enter, + formattedRecoveryKey = recoveryKey, + inProgress = submitAction.value.isLoading(), + ), + isSubmitEnabled = recoveryKey.isNotEmpty() && submitAction.value.isUninitialized(), + submitAction = submitAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.submitRecoveryKey( + recoveryKey: String, + action: MutableState> + ) = launch { + suspend { + encryptionService.fixRecoveryIssues(recoveryKey).getOrThrow() + }.runCatchingUpdatingState(action) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt new file mode 100644 index 0000000000..c9a631c11c --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt @@ -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. + */ + +package io.element.android.features.securebackup.impl.enter + +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.libraries.architecture.Async + +// Do not use default value, so no member get forgotten in the presenters. +data class SecureBackupEnterRecoveryKeyState( + val recoveryKeyViewState: RecoveryKeyViewState, + val isSubmitEnabled: Boolean, + val submitAction: Async, + val eventSink: (SecureBackupEnterRecoveryKeyEvents) -> Unit +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStateProvider.kt new file mode 100644 index 0000000000..375eafef73 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStateProvider.kt @@ -0,0 +1,48 @@ +/* + * 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.securebackup.impl.enter + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey +import io.element.android.libraries.architecture.Async + +open class SecureBackupEnterRecoveryKeyStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSecureBackupEnterRecoveryKeyState(recoveryKey = ""), + aSecureBackupEnterRecoveryKeyState(), + aSecureBackupEnterRecoveryKeyState(submitAction = Async.Loading()), + aSecureBackupEnterRecoveryKeyState(submitAction = Async.Failure(Exception("A Failure"))), + ) +} + +fun aSecureBackupEnterRecoveryKeyState( + recoveryKey: String = aFormattedRecoveryKey(), + isSubmitEnabled: Boolean = recoveryKey.isNotEmpty(), + submitAction: Async = Async.Uninitialized, +) = SecureBackupEnterRecoveryKeyState( + recoveryKeyViewState = RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Enter, + formattedRecoveryKey = recoveryKey, + inProgress = submitAction.isLoading(), + ), + isSubmitEnabled = isSubmitEnabled, + submitAction = submitAction, + eventSink = {} +) 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 new file mode 100644 index 0000000000..3fb95e7310 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt @@ -0,0 +1,134 @@ +/* + * 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.securebackup.impl.enter + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.securebackup.impl.R +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView +import io.element.android.libraries.architecture.Async +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.dialogs.ErrorDialog +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.utils.CommonDrawables +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun SecureBackupEnterRecoveryKeyView( + state: SecureBackupEnterRecoveryKeyState, + onDone: () -> Unit, + modifier: Modifier = Modifier, +) { + when (state.submitAction) { + Async.Uninitialized -> Unit + is Async.Failure -> ErrorDialog( + content = state.submitAction.error.message ?: state.submitAction.error.toString(), + onDismiss = { + state.eventSink(SecureBackupEnterRecoveryKeyEvents.ClearDialog) + } + ) + is Async.Loading -> Unit + is Async.Success -> LaunchedEffect(state.submitAction) { + onDone() + } + } + + HeaderFooterPage( + modifier = modifier, + header = { + HeaderContent() + }, + footer = { + BottomMenu( + state = state, + onSubmit = { + state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit) + }, + ) + } + ) { + Content( + state = state, + onChange = { + state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it)) + }) + } +} + +@Composable +private fun HeaderContent( + modifier: Modifier = Modifier, +) { + IconTitleSubtitleMolecule( + modifier = modifier.padding(top = 60.dp), + iconResourceId = CommonDrawables.ic_key, + title = stringResource(id = R.string.screen_recovery_key_confirm_title), + subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description), + ) +} + +@Composable +private fun BottomMenu( + state: SecureBackupEnterRecoveryKeyState, + onSubmit: () -> Unit, +) { + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 20.dp) + ) { + Button( + text = stringResource(id = CommonStrings.action_confirm), + enabled = state.isSubmitEnabled, + showProgress = state.submitAction.isLoading(), + modifier = Modifier.fillMaxWidth(), + onClick = onSubmit + ) + } +} + +@Composable +private fun Content( + state: SecureBackupEnterRecoveryKeyState, + onChange: ((String) -> Unit)?, +) { + RecoveryKeyView( + modifier = Modifier.padding(top = 52.dp), + state = state.recoveryKeyViewState, + onClick = null, + onChange = onChange, + ) +} + +@PreviewsDayNight +@Composable +internal fun SecureBackupEnterRecoveryKeyViewPreview( + @PreviewParameter(SecureBackupEnterRecoveryKeyStateProvider::class) state: SecureBackupEnterRecoveryKeyState +) = ElementPreview { + SecureBackupEnterRecoveryKeyView( + state = state, + onDone = {}, + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt new file mode 100644 index 0000000000..676214e698 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt @@ -0,0 +1,91 @@ +/* + * 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.securebackup.impl.root + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler +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.SecureBackupConfig +import io.element.android.libraries.di.SessionScope + +@ContributesNode(SessionScope::class) +class SecureBackupRootNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: SecureBackupRootPresenter, +) : Node( + buildContext = buildContext, + plugins = plugins +) { + + interface Callback : Plugin { + fun onSetupClicked() + fun onChangeClicked() + fun onDisableClicked() + fun onEnableClicked() + fun onConfirmRecoveryKeyClicked() + } + + private fun onSetupClicked() { + plugins().forEach { it.onSetupClicked() } + } + + private fun onChangeClicked() { + plugins().forEach { it.onChangeClicked() } + } + + private fun onDisableClicked() { + plugins().forEach { it.onDisableClicked() } + } + + private fun onEnableClicked() { + plugins().forEach { it.onEnableClicked() } + } + + private fun onConfirmRecoveryKeyClicked() { + plugins().forEach { it.onConfirmRecoveryKeyClicked() } + } + + private fun onLearnMoreClicked(uriHandler: UriHandler) { + uriHandler.openUri(SecureBackupConfig.LearnMoreUrl) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val uriHandler = LocalUriHandler.current + SecureBackupRootView( + state = state, + onBackPressed = ::navigateUp, + onSetupClicked = ::onSetupClicked, + onChangeClicked = ::onChangeClicked, + onEnableClicked = ::onEnableClicked, + onDisableClicked = ::onDisableClicked, + onConfirmRecoveryKeyClicked = ::onConfirmRecoveryKeyClicked, + onLearnMoreClicked = { onLearnMoreClicked(uriHandler) }, + modifier = modifier, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt new file mode 100644 index 0000000000..8a8fa6075c --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt @@ -0,0 +1,54 @@ +/* + * 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.securebackup.impl.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import io.element.android.features.securebackup.impl.loggerTagRoot +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import timber.log.Timber +import javax.inject.Inject + +class SecureBackupRootPresenter @Inject constructor( + private val encryptionService: EncryptionService, + private val buildMeta: BuildMeta, + private val snackbarDispatcher: SnackbarDispatcher, +) : Presenter { + + @Composable + override fun present(): SecureBackupRootState { + val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() + + val backupState by encryptionService.backupStateStateFlow.collectAsState() + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + + Timber.tag(loggerTagRoot.value).d("backupState: $backupState") + Timber.tag(loggerTagRoot.value).d("recoveryState: $recoveryState") + + return SecureBackupRootState( + backupState = backupState, + recoveryState = recoveryState, + appName = buildMeta.applicationName, + snackbarMessage = snackbarMessage, + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt new file mode 100644 index 0000000000..1eacd9e81a --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt @@ -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. + */ + +package io.element.android.features.securebackup.impl.root + +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.RecoveryState + +data class SecureBackupRootState( + val backupState: BackupState, + val recoveryState: RecoveryState, + val appName: String, + val snackbarMessage: SnackbarMessage?, +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt new file mode 100644 index 0000000000..ee15a40f50 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt @@ -0,0 +1,47 @@ +/* + * 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.securebackup.impl.root + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.RecoveryState + +open class SecureBackupRootStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSecureBackupRootState(backupState = BackupState.UNKNOWN), + aSecureBackupRootState(backupState = BackupState.ENABLED), + aSecureBackupRootState(backupState = BackupState.DISABLED), + aSecureBackupRootState(recoveryState = RecoveryState.UNKNOWN), + aSecureBackupRootState(recoveryState = RecoveryState.ENABLED), + aSecureBackupRootState(recoveryState = RecoveryState.DISABLED), + aSecureBackupRootState(recoveryState = RecoveryState.INCOMPLETE), + // Add other states here + ) +} + +fun aSecureBackupRootState( + backupState: BackupState = BackupState.UNKNOWN, + recoveryState: RecoveryState = RecoveryState.UNKNOWN, + snackbarMessage: SnackbarMessage? = null, +) = SecureBackupRootState( + backupState = backupState, + recoveryState = recoveryState, + appName = "Element", + snackbarMessage = snackbarMessage, +) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt new file mode 100644 index 0000000000..dd352ca99e --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt @@ -0,0 +1,141 @@ +/* + * 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.securebackup.impl.root + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.securebackup.impl.R +import io.element.android.libraries.designsystem.components.async.AsyncLoading +import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider +import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.components.preferences.PreferenceText +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.utils.snackbar.SnackbarHost +import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.RecoveryState +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun SecureBackupRootView( + state: SecureBackupRootState, + onBackPressed: () -> Unit, + onSetupClicked: () -> Unit, + onChangeClicked: () -> Unit, + onEnableClicked: () -> Unit, + onDisableClicked: () -> Unit, + onConfirmRecoveryKeyClicked: () -> Unit, + onLearnMoreClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) + + PreferencePage( + modifier = modifier, + onBackPressed = onBackPressed, + title = stringResource(id = CommonStrings.common_chat_backup), + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { + val text = buildAnnotatedStringWithStyledPart( + fullTextRes = R.string.screen_chat_backup_key_backup_description, + coloredTextRes = CommonStrings.action_learn_more, + color = ElementTheme.colors.textPrimary, + underline = false, + bold = true, + ) + PreferenceText( + title = stringResource(id = R.string.screen_chat_backup_key_backup_title), + subtitleAnnotated = text, + onClick = onLearnMoreClicked, + ) + + // Disable / Enable backup + when (state.backupState) { + BackupState.UNKNOWN -> Unit + BackupState.DISABLED -> { + PreferenceText( + title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable), + onClick = onEnableClicked, + ) + } + BackupState.CREATING, + BackupState.ENABLING, + BackupState.RESUMING, + BackupState.ENABLED, + BackupState.DOWNLOADING -> { + PreferenceText( + title = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable), + tintColor = ElementTheme.colors.textCriticalPrimary, + onClick = onDisableClicked, + ) + } + BackupState.DISABLING -> { + AsyncLoading() + } + } + + PreferenceDivider() + + // Setup recovery + when (state.recoveryState) { + RecoveryState.UNKNOWN, + RecoveryState.DISABLED -> { + PreferenceText( + title = stringResource(id = R.string.screen_chat_backup_recovery_action_setup), + subtitle = stringResource(id = R.string.screen_chat_backup_recovery_action_setup_description, state.appName), + onClick = onSetupClicked, + showEndBadge = true, + ) + } + RecoveryState.ENABLED -> { + PreferenceText( + title = stringResource(id = R.string.screen_chat_backup_recovery_action_change), + onClick = onChangeClicked, + ) + } + RecoveryState.INCOMPLETE -> + PreferenceText( + title = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm), + subtitle = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm_description), + showEndBadge = true, + onClick = onConfirmRecoveryKeyClicked, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun SecureBackupRootViewPreview( + @PreviewParameter(SecureBackupRootStateProvider::class) state: SecureBackupRootState +) = ElementPreview { + SecureBackupRootView( + state = state, + onBackPressed = {}, + onSetupClicked = {}, + onChangeClicked = {}, + onEnableClicked = {}, + onDisableClicked = {}, + onConfirmRecoveryKeyClicked = {}, + onLearnMoreClicked = {}, + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvents.kt new file mode 100644 index 0000000000..746060f8e1 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvents.kt @@ -0,0 +1,24 @@ +/* + * 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.securebackup.impl.setup + +sealed interface SecureBackupSetupEvents { + data object CreateRecoveryKey : SecureBackupSetupEvents + data object RecoveryKeyHasBeenSaved : SecureBackupSetupEvents + data object Done : SecureBackupSetupEvents + data object DismissDialog : SecureBackupSetupEvents +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt new file mode 100644 index 0000000000..15effd9045 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt @@ -0,0 +1,77 @@ +/* + * 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.securebackup.impl.setup + +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 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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +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 SecureBackupSetupNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: SecureBackupSetupPresenter.Factory, + private val snackbarDispatcher: SnackbarDispatcher, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val isChangeRecoveryKeyUserStory: Boolean, + ) : NodeInputs + + private val inputs = inputs() + + private val presenter = presenterFactory.create(inputs.isChangeRecoveryKeyUserStory) + + @Composable + override fun View(modifier: Modifier) { + val coroutineScope = rememberCoroutineScope() + val state = presenter.present() + SecureBackupSetupView( + state = state, + onDone = { + coroutineScope.postSuccessSnackbar() + navigateUp() + }, + modifier = modifier, + ) + } + + private fun CoroutineScope.postSuccessSnackbar() = launch { + snackbarDispatcher.post( + SnackbarMessage( + messageResId = if (inputs.isChangeRecoveryKeyUserStory) + R.string.screen_recovery_key_change_success + else + R.string.screen_recovery_key_setup_success + ) + ) + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt new file mode 100644 index 0000000000..1bba716bb1 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt @@ -0,0 +1,142 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.securebackup.impl.setup + +import androidx.compose.runtime.Composable +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.StateAndDispatch +import com.freeletics.flowredux.compose.rememberStateAndDispatch +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.securebackup.impl.loggerTagSetup +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import timber.log.Timber + +class SecureBackupSetupPresenter @AssistedInject constructor( + @Assisted private val isChangeRecoveryKeyUserStory: Boolean, + private val stateMachine: SecureBackupSetupStateMachine, + private val encryptionService: EncryptionService, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(isChangeRecoveryKeyUserStory: Boolean): SecureBackupSetupPresenter + } + + @Composable + override fun present(): SecureBackupSetupState { + val coroutineScope = rememberCoroutineScope() + val stateAndDispatch = stateMachine.rememberStateAndDispatch() + val setupState by remember { + derivedStateOf { stateAndDispatch.state.value.toSetupState() } + } + var showSaveConfirmationDialog by remember { mutableStateOf(false) } + + fun handleEvents(event: SecureBackupSetupEvents) { + when (event) { + SecureBackupSetupEvents.CreateRecoveryKey -> { + coroutineScope.createOrChangeRecoveryKey(stateAndDispatch) + } + SecureBackupSetupEvents.RecoveryKeyHasBeenSaved -> + stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey) + SecureBackupSetupEvents.DismissDialog -> { + showSaveConfirmationDialog = false + } + SecureBackupSetupEvents.Done -> { + showSaveConfirmationDialog = true + } + } + } + + val recoveryKeyViewState = RecoveryKeyViewState( + recoveryKeyUserStory = if (isChangeRecoveryKeyUserStory) RecoveryKeyUserStory.Change else RecoveryKeyUserStory.Setup, + formattedRecoveryKey = setupState.recoveryKey(), + inProgress = setupState is SetupState.Creating, + ) + + return SecureBackupSetupState( + isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory, + recoveryKeyViewState = recoveryKeyViewState, + setupState = setupState, + showSaveConfirmationDialog = showSaveConfirmationDialog, + eventSink = ::handleEvents + ) + } + + private fun SecureBackupSetupStateMachine.State?.toSetupState(): SetupState { + return when (this) { + null, + SecureBackupSetupStateMachine.State.Initial -> SetupState.Init + SecureBackupSetupStateMachine.State.CreatingKey -> SetupState.Creating + is SecureBackupSetupStateMachine.State.KeyCreated -> SetupState.Created(formattedRecoveryKey = key) + is SecureBackupSetupStateMachine.State.KeyCreatedAndSaved -> SetupState.CreatedAndSaved(formattedRecoveryKey = key) + } + } + + private fun CoroutineScope.createOrChangeRecoveryKey( + stateAndDispatch: StateAndDispatch + ) = launch { + stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserCreatesKey) + if (isChangeRecoveryKeyUserStory) { + Timber.tag(loggerTagSetup.value).d("Calling encryptionService.resetRecoveryKey()") + encryptionService.resetRecoveryKey().fold( + onSuccess = { + stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.SdkHasCreatedKey(it)) + }, + onFailure = { + stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.SdkError(it)) + } + ) + } else { + observeEncryptionService(stateAndDispatch) + Timber.tag(loggerTagSetup.value).d("Calling encryptionService.enableRecovery()") + encryptionService.enableRecovery(waitForBackupsToUpload = false) + } + } + + private fun CoroutineScope.observeEncryptionService( + stateAndDispatch: StateAndDispatch + ) = launch { + encryptionService.enableRecoveryProgressStateFlow.collect { enableRecoveryProgress -> + Timber.tag(loggerTagSetup.value).d("New enableRecoveryProgress: ${enableRecoveryProgress.javaClass.simpleName}") + when (enableRecoveryProgress) { + EnableRecoveryProgress.Unknown, + is EnableRecoveryProgress.BackingUp, + EnableRecoveryProgress.CreatingBackup, + EnableRecoveryProgress.CreatingRecoveryKey -> + Unit + is EnableRecoveryProgress.Done -> + stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.SdkHasCreatedKey(enableRecoveryProgress.recoveryKey)) + } + } + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt new file mode 100644 index 0000000000..918d52da54 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt @@ -0,0 +1,41 @@ +/* + * 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.securebackup.impl.setup + +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState + +// Do not use default value, so no member get forgotten in the presenters. +data class SecureBackupSetupState( + val isChangeRecoveryKeyUserStory: Boolean, + val recoveryKeyViewState: RecoveryKeyViewState, + val showSaveConfirmationDialog: Boolean, + val setupState: SetupState, + val eventSink: (SecureBackupSetupEvents) -> Unit +) + +sealed interface SetupState { + data object Init : SetupState + data object Creating : SetupState + data class Created(val formattedRecoveryKey: String) : SetupState + data class CreatedAndSaved(val formattedRecoveryKey: String) : SetupState +} + +fun SetupState.recoveryKey(): String? = when (this) { + is SetupState.Created -> formattedRecoveryKey + is SetupState.CreatedAndSaved -> formattedRecoveryKey + else -> null +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateMachine.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateMachine.kt new file mode 100644 index 0000000000..7e2c633a61 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateMachine.kt @@ -0,0 +1,70 @@ +/* + * 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. + */ + +@file:Suppress("WildcardImport") +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.securebackup.impl.setup + +import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import kotlinx.coroutines.ExperimentalCoroutinesApi +import javax.inject.Inject +import com.freeletics.flowredux.dsl.State as MachineState + +class SecureBackupSetupStateMachine @Inject constructor( +) : FlowReduxStateMachine( + initialState = State.Initial +) { + + init { + spec { + inState { + on { _: Event.UserCreatesKey, state: MachineState -> + state.override { State.CreatingKey } + } + } + inState { + on { _: Event.SdkError, state: MachineState -> + state.override { State.Initial } + } + on { event: Event.SdkHasCreatedKey, state: MachineState -> + state.override { State.KeyCreated(event.key) } + } + } + inState { + on { _: Event.UserSavedKey, state: MachineState -> + state.override { State.KeyCreatedAndSaved(state.snapshot.key) } + } + } + inState { + } + } + } + + sealed interface State { + data object Initial : State + data object CreatingKey : State + data class KeyCreated(val key: String) : State + data class KeyCreatedAndSaved(val key: String) : State + } + + sealed interface Event { + data object UserCreatesKey : Event + data class SdkHasCreatedKey(val key: String) : Event + data class SdkError(val throwable: Throwable) : Event + data object UserSavedKey : Event + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateProvider.kt new file mode 100644 index 0000000000..3aa1f0a7c3 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateProvider.kt @@ -0,0 +1,56 @@ +/* + * 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.securebackup.impl.setup + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey + +open class SecureBackupSetupStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSecureBackupSetupState(setupState = SetupState.Init), + aSecureBackupSetupState(setupState = SetupState.Creating), + aSecureBackupSetupState(setupState = SetupState.Created(aFormattedRecoveryKey())), + aSecureBackupSetupState(setupState = SetupState.CreatedAndSaved(aFormattedRecoveryKey())), + aSecureBackupSetupState( + setupState = SetupState.CreatedAndSaved(aFormattedRecoveryKey()), + showSaveConfirmationDialog = true, + ), + // Add other states here + ) +} + +fun aSecureBackupSetupState( + setupState: SetupState = SetupState.Init, + showSaveConfirmationDialog: Boolean = false, +) = SecureBackupSetupState( + isChangeRecoveryKeyUserStory = false, + setupState = setupState, + showSaveConfirmationDialog = showSaveConfirmationDialog, + recoveryKeyViewState = setupState.toRecoveryKeyViewState(), + eventSink = {} +) + +private fun SetupState.toRecoveryKeyViewState(): RecoveryKeyViewState { + return RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Setup, + formattedRecoveryKey = recoveryKey(), + inProgress = this is SetupState.Creating, + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt new file mode 100644 index 0000000000..0e1fa3b824 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt @@ -0,0 +1,207 @@ +/* + * 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.securebackup.impl.setup + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.securebackup.impl.R +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.libraries.androidutils.system.copyToClipboard +import io.element.android.libraries.androidutils.system.startSharePlainTextIntent +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.dialogs.ConfirmationDialog +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.IconSource +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun SecureBackupSetupView( + state: SecureBackupSetupState, + onDone: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + HeaderFooterPage( + modifier = modifier, + header = { + HeaderContent(state = state) + }, + footer = { + val chooserTitle = stringResource(id = R.string.screen_recovery_key_save_action) + BottomMenu( + state = state, + onSaveClicked = { key -> + context.startSharePlainTextIntent( + activityResultLauncher = null, + chooserTitle = chooserTitle, + text = key, + ) + state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved) + }, + onDone = { + if (state.setupState is SetupState.CreatedAndSaved) { + onDone() + } else { + state.eventSink.invoke(SecureBackupSetupEvents.Done) + } + }, + ) + } + ) { + val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey + val clickLambda = if (formattedRecoveryKey != null) { + { + context.copyToClipboard( + formattedRecoveryKey, + context.getString(R.string.screen_recovery_key_copied_to_clipboard) + ) + state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved) + } + } else { + if (!state.recoveryKeyViewState.inProgress) { + { + state.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey) + } + } else { + null + } + } + Content(state = state.recoveryKeyViewState, onClick = clickLambda) + } + + if (state.showSaveConfirmationDialog) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_recovery_key_setup_confirmation_title), + content = stringResource(id = R.string.screen_recovery_key_setup_confirmation_description), + submitText = stringResource(id = CommonStrings.action_continue), + onSubmitClicked = onDone, + onDismiss = { + state.eventSink.invoke(SecureBackupSetupEvents.DismissDialog) + } + ) + } +} + +@Composable +private fun HeaderContent( + state: SecureBackupSetupState, + modifier: Modifier = Modifier, +) { + val setupState = state.setupState + val title = when (setupState) { + SetupState.Init, + SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory) + stringResource(id = R.string.screen_recovery_key_change_title) + else + stringResource(id = R.string.screen_recovery_key_setup_title) + is SetupState.Created, + is SetupState.CreatedAndSaved -> + stringResource(id = R.string.screen_recovery_key_save_title) + } + val subTitle = when (setupState) { + SetupState.Init, + SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory) + stringResource(id = R.string.screen_recovery_key_change_description) + else + stringResource(id = R.string.screen_recovery_key_setup_description) + is SetupState.Created, + is SetupState.CreatedAndSaved -> + stringResource(id = R.string.screen_recovery_key_save_description) + } + IconTitleSubtitleMolecule( + modifier = modifier.padding(top = 60.dp), + iconResourceId = CommonDrawables.ic_key, + title = title, + subTitle = subTitle, + ) +} + +@Composable +private fun BottomMenu( + state: SecureBackupSetupState, + onSaveClicked: (String) -> Unit, + onDone: () -> Unit, +) { + val setupState = state.setupState + ButtonColumnMolecule( + modifier = Modifier.padding(bottom = 20.dp) + ) { + when (setupState) { + SetupState.Init, + SetupState.Creating -> { + Button( + text = stringResource(id = CommonStrings.action_done), + enabled = false, + modifier = Modifier.fillMaxWidth(), + onClick = onDone + ) + } + is SetupState.Created, + is SetupState.CreatedAndSaved -> { + OutlinedButton( + text = stringResource(id = R.string.screen_recovery_key_save_action), + leadingIcon = IconSource.Resource(CommonDrawables.ic_compound_download), + modifier = Modifier.fillMaxWidth(), + onClick = { onSaveClicked(setupState.recoveryKey()!!) }, + ) + Button( + text = stringResource(id = CommonStrings.action_done), + modifier = Modifier.fillMaxWidth(), + onClick = onDone, + ) + } + } + } +} + +@Composable +private fun Content( + state: RecoveryKeyViewState, + onClick: (() -> Unit)?, +) { + val modifier = Modifier.padding(top = 52.dp) + RecoveryKeyView( + modifier = modifier, + state = state, + onClick = onClick, + onChange = null, + ) +} + +@PreviewsDayNight +@Composable +internal fun SecureBackupSetupViewPreview( + @PreviewParameter(SecureBackupSetupStateProvider::class) state: SecureBackupSetupState +) = ElementPreview { + SecureBackupSetupView( + state = state, + onDone = {}, + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt new file mode 100644 index 0000000000..0f7408700f --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt @@ -0,0 +1,37 @@ +/* + * 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.securebackup.impl.setup + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@PreviewsDayNight +@Composable +internal fun SecureBackupSetupViewChangePreview( + @PreviewParameter(SecureBackupSetupStateProvider::class) state: SecureBackupSetupState +) = ElementPreview { + SecureBackupSetupView( + state = state.copy( + isChangeRecoveryKeyUserStory = true, + recoveryKeyViewState = state.recoveryKeyViewState.copy(recoveryKeyUserStory = RecoveryKeyUserStory.Change), + ), + onDone = {}, + ) +} 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 new file mode 100644 index 0000000000..24e70d3b8c --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt @@ -0,0 +1,221 @@ +/* + * 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.securebackup.impl.setup.views + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.progressSemantics +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.securebackup.impl.R +import io.element.android.features.securebackup.impl.tools.RecoveryKeyVisualTransformation +import io.element.android.libraries.designsystem.modifiers.clickableIfNotNull +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +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.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun RecoveryKeyView( + state: RecoveryKeyViewState, + onClick: (() -> Unit)?, + onChange: ((String) -> Unit)?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(id = CommonStrings.common_recovery_key), + modifier = Modifier.padding(start = 16.dp), + style = ElementTheme.typography.fontBodyMdRegular, + ) + RecoveryKeyContent(state, onClick, onChange) + RecoveryKeyFooter(state) + } +} + +@Composable +private fun RecoveryKeyContent( + state: RecoveryKeyViewState, + onClick: (() -> Unit)?, + onChange: ((String) -> Unit)?, +) { + when (state.recoveryKeyUserStory) { + RecoveryKeyUserStory.Setup, + RecoveryKeyUserStory.Change -> RecoveryKeyStaticContent(state, onClick) + RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange) + } +} + +@Composable +private fun RecoveryKeyStaticContent( + state: RecoveryKeyViewState, + onClick: (() -> Unit)?, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background( + color = ElementTheme.colors.bgSubtleSecondary, + shape = RoundedCornerShape(14.dp) + ) + .clickableIfNotNull(onClick) + .padding(horizontal = 16.dp, vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (state.formattedRecoveryKey != null) { + Text( + text = state.formattedRecoveryKey, + modifier = Modifier.weight(1f), + ) + Icon( + resourceId = CommonDrawables.ic_september_copy, + contentDescription = stringResource(id = CommonStrings.action_copy), + tint = ElementTheme.colors.iconSecondary, + ) + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 11.dp) + ) { + if (state.inProgress) { + CircularProgressIndicator( + modifier = Modifier + .progressSemantics() + .padding(end = 8.dp) + .size(16.dp), + color = ElementTheme.colors.textPrimary, + strokeWidth = 1.5.dp, + ) + } + Text( + text = stringResource( + id = when { + state.inProgress -> R.string.screen_recovery_key_generating_key + state.recoveryKeyUserStory == RecoveryKeyUserStory.Change -> R.string.screen_recovery_key_change_generate_key + else -> R.string.screen_recovery_key_setup_generate_key + } + ), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyLgMedium, + ) + } + } + } +} + +@Composable +private fun RecoveryKeyFormContent(state: RecoveryKeyViewState, onChange: ((String) -> Unit)?) { + onChange ?: error("onChange should not be null") + val recoveryKeyVisualTransformation = remember { + RecoveryKeyVisualTransformation() + } + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + minLines = 2, + value = state.formattedRecoveryKey.orEmpty(), + onValueChange = onChange, + enabled = state.inProgress.not(), + visualTransformation = recoveryKeyVisualTransformation, + label = { Text(text = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder)) } + ) +} + +@Composable +private fun RecoveryKeyFooter(state: RecoveryKeyViewState) { + when (state.recoveryKeyUserStory) { + RecoveryKeyUserStory.Setup, + RecoveryKeyUserStory.Change -> { + if (state.formattedRecoveryKey == null) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + resourceId = CommonDrawables.ic_compound_info, + contentDescription = null, + tint = ElementTheme.colors.iconSecondary, + modifier = Modifier + .padding(start = 16.dp) + .size(20.dp), + ) + Text( + text = stringResource( + id = if (state.recoveryKeyUserStory == RecoveryKeyUserStory.Change) + R.string.screen_recovery_key_change_generate_key_description + else + R.string.screen_recovery_key_setup_generate_key_description + ), + color = ElementTheme.colors.textSecondary, + modifier = Modifier.padding(start = 8.dp), + style = ElementTheme.typography.fontBodySmRegular, + ) + } + } else { + Text( + text = stringResource(id = R.string.screen_recovery_key_save_key_description), + color = ElementTheme.colors.textSecondary, + modifier = Modifier.padding(start = 16.dp), + style = ElementTheme.typography.fontBodySmRegular, + ) + } + } + RecoveryKeyUserStory.Enter -> { + Text( + text = stringResource(id = R.string.screen_recovery_key_confirm_key_description), + color = ElementTheme.colors.textSecondary, + modifier = Modifier.padding(start = 16.dp), + style = ElementTheme.typography.fontBodySmRegular, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun RecoveryKeyViewPreview( + @PreviewParameter(RecoveryKeyViewStateProvider::class) state: RecoveryKeyViewState +) = ElementPreview { + RecoveryKeyView( + state = state, + onClick = {}, + onChange = {}, + ) +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewState.kt new file mode 100644 index 0000000000..fee10d9f48 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewState.kt @@ -0,0 +1,29 @@ +/* + * 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.securebackup.impl.setup.views + +data class RecoveryKeyViewState( + val recoveryKeyUserStory: RecoveryKeyUserStory, + val formattedRecoveryKey: String?, + val inProgress: Boolean, +) + +enum class RecoveryKeyUserStory { + Setup, + Change, + Enter, +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStateProvider.kt new file mode 100644 index 0000000000..f3cc6764c0 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStateProvider.kt @@ -0,0 +1,47 @@ +/* + * 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.securebackup.impl.setup.views + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class RecoveryKeyViewStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(RecoveryKeyUserStory.Setup, RecoveryKeyUserStory.Change, RecoveryKeyUserStory.Enter) + .flatMap { + sequenceOf( + aRecoveryKeyViewState(recoveryKeyUserStory = it), + aRecoveryKeyViewState(recoveryKeyUserStory = it, inProgress = true), + aRecoveryKeyViewState(recoveryKeyUserStory = it, formattedRecoveryKey = aFormattedRecoveryKey()), + aRecoveryKeyViewState(recoveryKeyUserStory = it, formattedRecoveryKey = aFormattedRecoveryKey(), inProgress = true), + // Add other states here + ) + } +} + +fun aRecoveryKeyViewState( + recoveryKeyUserStory: RecoveryKeyUserStory = RecoveryKeyUserStory.Setup, + formattedRecoveryKey: String? = null, + inProgress: Boolean = false, +) = RecoveryKeyViewState( + recoveryKeyUserStory = recoveryKeyUserStory, + formattedRecoveryKey = formattedRecoveryKey, + inProgress = inProgress, +) + +internal fun aFormattedRecoveryKey(): String { + return "Estm dfyU adhD h8y6 Estm dfyU adhD h8y6 Estm dfyU adhD h8y6" +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyVisualTransformation.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyVisualTransformation.kt new file mode 100644 index 0000000000..f15acaa458 --- /dev/null +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyVisualTransformation.kt @@ -0,0 +1,51 @@ +/* + * 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.securebackup.impl.tools + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +class RecoveryKeyVisualTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + return TransformedText( + text = AnnotatedString( + text.text + .chunked(4) + .joinToString(separator = " ") + ), + offsetMapping = RecoveryKeyOffsetMapping(text.text), + ) + } + + class RecoveryKeyOffsetMapping(private val text: String) : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + if (offset == 0) return 0 + val numberOfChunks = offset / 4 + return if (offset == text.length && offset % 4 == 0) + offset + numberOfChunks - 1 + else + offset + numberOfChunks + } + + override fun transformedToOriginal(offset: Int): Int { + val numberOfChunks = offset / 5 + return offset - numberOfChunks + } + } +} diff --git a/features/securebackup/impl/src/main/res/values-sk/translations.xml b/features/securebackup/impl/src/main/res/values-sk/translations.xml new file mode 100644 index 0000000000..7c702d0be2 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-sk/translations.xml @@ -0,0 +1,42 @@ + + + "Vypnúť zálohovanie" + "Zapnúť zálohovanie" + "Zálohovanie zaisťuje, že nestratíte históriu správ. %1$s." + "Zálohovanie" + "Zmeniť kľúč na obnovenie" + "Potvrdiť kľúč na obnovenie" + "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í." + "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?" + "Vypnutím zálohovania sa odstráni aktuálna záloha šifrovacích kľúčov a vypnú sa ďalšie bezpečnostné funkcie. V tomto prípade:" + "Na nových zariadeniach nebudete mať zašifrovanú históriu správ" + "Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite zo všetkých %1$s zariadení" + "Ste si istí, že chcete vypnúť zálohovanie?" + "Získajte nový kľúč na obnovenie, ak ste stratili svoj existujúci. Po zmene kľúča na obnovenie už starý kľúč nebude fungovať." + "Vygenerovať nový kľúč na obnovenie" + "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." + "Zadajte 48-znakový kód." + "Zadať…" + "Kľúč na obnovu potvrdený" + "Potvrďte kľúč na obnovenie" + "Skopírovaný kľúč na obnovenie" + "Generovanie…" + "Uložiť kľúč na obnovenie" + "Zapíšte si kľúč na obnovenie na bezpečné miesto alebo ho uložte do správcu hesiel." + "Ťuknutím skopírujte kľúč na obnovenie" + "Uložte svoj kľúč na obnovenie" + "Po tomto kroku nebudete mať prístup k novému kľúču na obnovenie." + "Uložili ste kľúč na obnovenie?" + "Vaša záloha konverzácie je chránená kľúčom na obnovenie. Ak potrebujete nový kľúč na obnovenie, po nastavení si ho môžete znova vytvoriť výberom položky „Zmeniť kľúč na obnovenie“." + "Vygenerujte si váš kľúč na obnovenie" + "Uistite sa, že kľúč na obnovenie môžete uložiť niekde v bezpečí" + "Úspešné nastavenie obnovy" + "Nastaviť obnovenie" + diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..67dd2ec045 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values/localazy.xml @@ -0,0 +1,42 @@ + + + "Turn off backup" + "Turn on backup" + "Backup ensures that you don\'t lose your message history. %1$s." + "Backup" + "Change recovery key" + "Confirm recovery key" + "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." + "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?" + "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:" + "Not have encrypted message history on new devices" + "Lose access to your encrypted messages if you are signed out of %1$s everywhere" + "Are you sure you want to turn off backup?" + "Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work." + "Generate a new recovery key" + "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." + "Enter the 48 character code." + "Enter…" + "Recovery key confirmed" + "Confirm your recovery key" + "Copied recovery key" + "Generating…" + "Save recovery key" + "Write down your recovery key somewhere safe or save it in a password manager." + "Tap to copy recovery key" + "Save your recovery key" + "You will not be able to access your new recovery key after this step." + "Have you saved your recovery key?" + "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’." + "Generate your recovery key" + "Make sure you can store your recovery key somewhere safe" + "Recovery setup successful" + "Set up recovery" + diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt new file mode 100644 index 0000000000..8e42ee7efe --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt @@ -0,0 +1,129 @@ +/* + * 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.securebackup.impl.disable + +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.Async +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SecureBackupDisablePresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createSecureBackupDisablePresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN) + assertThat(initialState.disableAction).isEqualTo(Async.Uninitialized) + assertThat(initialState.showConfirmationDialog).isFalse() + assertThat(initialState.appName).isEqualTo("Element") + } + } + + @Test + fun `present - user delete backup and cancel`() = runTest { + val presenter = createSecureBackupDisablePresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.showConfirmationDialog).isFalse() + initialState.eventSink(SecureBackupDisableEvents.DisableBackup(force = false)) + val state = awaitItem() + assertThat(state.showConfirmationDialog).isTrue() + initialState.eventSink(SecureBackupDisableEvents.DismissDialogs) + val finalState = awaitItem() + assertThat(finalState.showConfirmationDialog).isFalse() + } + } + + @Test + fun `present - user delete backup success`() = runTest { + val presenter = createSecureBackupDisablePresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.showConfirmationDialog).isFalse() + initialState.eventSink(SecureBackupDisableEvents.DisableBackup(force = false)) + val state = awaitItem() + assertThat(state.showConfirmationDialog).isTrue() + initialState.eventSink(SecureBackupDisableEvents.DisableBackup(force = true)) + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState.showConfirmationDialog).isFalse() + assertThat(loadingState.disableAction).isInstanceOf(Async.Loading::class.java) + val finalState = awaitItem() + assertThat(finalState.disableAction).isEqualTo(Async.Success(Unit)) + } + } + + @Test + fun `present - user delete backup error`() = runTest { + val encryptionService = FakeEncryptionService().apply { + givenDisableRecoveryFailure(Exception("failure")) + } + val presenter = createSecureBackupDisablePresenter( + encryptionService = encryptionService + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.showConfirmationDialog).isFalse() + initialState.eventSink(SecureBackupDisableEvents.DisableBackup(force = false)) + val state = awaitItem() + assertThat(state.showConfirmationDialog).isTrue() + initialState.eventSink(SecureBackupDisableEvents.DisableBackup(force = true)) + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState.showConfirmationDialog).isFalse() + assertThat(loadingState.disableAction).isInstanceOf(Async.Loading::class.java) + val errorState = awaitItem() + assertThat(errorState.disableAction).isInstanceOf(Async.Failure::class.java) + errorState.eventSink(SecureBackupDisableEvents.DismissDialogs) + val finalState = awaitItem() + assertThat(finalState.disableAction).isEqualTo(Async.Uninitialized) + } + } + + private fun createSecureBackupDisablePresenter( + encryptionService: EncryptionService = FakeEncryptionService(), + appName: String = "Element", + ): SecureBackupDisablePresenter { + return SecureBackupDisablePresenter( + encryptionService = encryptionService, + buildMeta = aBuildMeta( + applicationName = appName, + ) + ) + } +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenterTest.kt new file mode 100644 index 0000000000..5aa223cf93 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnablePresenterTest.kt @@ -0,0 +1,66 @@ +/* + * 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.securebackup.impl.enable + +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.Async +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SecureBackupEnablePresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.enableAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - user enable backup`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(SecureBackupEnableEvents.EnableBackup) + val loadingState = awaitItem() + assertThat(loadingState.enableAction).isInstanceOf(Async.Loading::class.java) + val finalState = awaitItem() + assertThat(finalState.enableAction).isEqualTo(Async.Success(Unit)) + } + } + + private fun createPresenter( + encryptionService: EncryptionService = FakeEncryptionService(), + ) = SecureBackupEnablePresenter( + encryptionService = encryptionService, + ) +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenterTest.kt new file mode 100644 index 0000000000..bcdbb6bbe7 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenterTest.kt @@ -0,0 +1,103 @@ +/* + * 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.securebackup.impl.enter + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SecureBackupEnterRecoveryKeyPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isSubmitEnabled).isFalse() + assertThat(initialState.submitAction).isEqualTo(Async.Uninitialized) + assertThat(initialState.recoveryKeyViewState).isEqualTo( + RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Enter, + formattedRecoveryKey = "", + inProgress = false, + ) + ) + } + } + + @Test + fun `present - enter recovery key`() = runTest { + val encryptionService = FakeEncryptionService() + val presenter = createPresenter(encryptionService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange("1234")) + val withRecoveryKeyState = awaitItem() + assertThat(withRecoveryKeyState.isSubmitEnabled).isTrue() + assertThat(withRecoveryKeyState.recoveryKeyViewState).isEqualTo( + RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Enter, + formattedRecoveryKey = "1234", + inProgress = false, + ) + ) + encryptionService.givenFixRecoveryIssuesFailure(AN_EXCEPTION) + withRecoveryKeyState.eventSink(SecureBackupEnterRecoveryKeyEvents.Submit) + val loadingState = awaitItem() + assertThat(loadingState.submitAction).isEqualTo(Async.Loading()) + assertThat(loadingState.isSubmitEnabled).isFalse() + val errorState = awaitItem() + assertThat(errorState.submitAction).isEqualTo(Async.Failure(AN_EXCEPTION)) + assertThat(errorState.isSubmitEnabled).isFalse() + errorState.eventSink(SecureBackupEnterRecoveryKeyEvents.ClearDialog) + val clearedState = awaitItem() + assertThat(clearedState.submitAction).isEqualTo(Async.Uninitialized) + assertThat(clearedState.isSubmitEnabled).isTrue() + encryptionService.givenFixRecoveryIssuesFailure(null) + clearedState.eventSink(SecureBackupEnterRecoveryKeyEvents.Submit) + val loadingState2 = awaitItem() + assertThat(loadingState2.submitAction).isEqualTo(Async.Loading()) + assertThat(loadingState2.isSubmitEnabled).isFalse() + val finalState = awaitItem() + assertThat(finalState.submitAction).isEqualTo(Async.Success(Unit)) + assertThat(finalState.isSubmitEnabled).isFalse() + } + } + + private fun createPresenter( + encryptionService: EncryptionService = FakeEncryptionService(), + ) = SecureBackupEnterRecoveryKeyPresenter( + encryptionService = encryptionService, + ) +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt new file mode 100644 index 0000000000..93031b6d2a --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt @@ -0,0 +1,59 @@ +/* + * 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.securebackup.impl.root + +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.designsystem.utils.snackbar.SnackbarDispatcher +import io.element.android.libraries.matrix.api.encryption.BackupState +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SecureBackupRootPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createSecureBackupRootPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN) + assertThat(initialState.appName).isEqualTo("Element") + } + } + + private fun createSecureBackupRootPresenter( + encryptionService: EncryptionService = FakeEncryptionService(), + appName: String = "Element", + ): SecureBackupRootPresenter { + return SecureBackupRootPresenter( + encryptionService = encryptionService, + buildMeta = aBuildMeta(applicationName = appName), + snackbarDispatcher = SnackbarDispatcher(), + ) + } +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt new file mode 100644 index 0000000000..f4071c6645 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt @@ -0,0 +1,173 @@ +/* + * 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.securebackup.impl.setup + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory +import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState +import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress +import io.element.android.libraries.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.test.A_RECOVERY_KEY +import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SecureBackupSetupPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createSecureBackupSetupPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isChangeRecoveryKeyUserStory).isFalse() + assertThat(initialState.setupState).isEqualTo(SetupState.Init) + assertThat(initialState.showSaveConfirmationDialog).isFalse() + assertThat(initialState.recoveryKeyViewState).isEqualTo( + RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Setup, + formattedRecoveryKey = null, + inProgress = false, + ) + ) + } + } + + @Test + fun `present - create recovery key and save it`() = runTest { + val encryptionService = FakeEncryptionService() + val presenter = createSecureBackupSetupPresenter( + encryptionService = encryptionService + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey) + val creatingState = awaitItem() + assertThat(creatingState.setupState).isEqualTo(SetupState.Creating) + assertThat(creatingState.recoveryKeyViewState).isEqualTo( + RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Setup, + formattedRecoveryKey = null, + inProgress = true, + ) + ) + encryptionService.emitEnableRecoveryProgress(EnableRecoveryProgress.Done(A_RECOVERY_KEY)) + val createdState = awaitItem() + assertThat(createdState.setupState).isEqualTo(SetupState.Created(A_RECOVERY_KEY)) + assertThat(createdState.recoveryKeyViewState).isEqualTo( + RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Setup, + formattedRecoveryKey = A_RECOVERY_KEY, + inProgress = false, + ) + ) + createdState.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved) + val createdAndSaveState = awaitItem() + assertThat(createdAndSaveState.setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java) + createdAndSaveState.eventSink.invoke(SecureBackupSetupEvents.Done) + val doneState = awaitItem() + assertThat(doneState.showSaveConfirmationDialog).isTrue() + doneState.eventSink.invoke(SecureBackupSetupEvents.DismissDialog) + val doneStateCancelled = awaitItem() + assertThat(doneStateCancelled.showSaveConfirmationDialog).isFalse() + } + } + + @Test + fun `present - initial state change key`() = runTest { + val presenter = createSecureBackupSetupPresenter( + isChangeRecoveryKeyUserStory = true, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isChangeRecoveryKeyUserStory).isTrue() + assertThat(initialState.setupState).isEqualTo(SetupState.Init) + assertThat(initialState.recoveryKeyViewState).isEqualTo( + RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Change, + formattedRecoveryKey = null, + inProgress = false, + ) + ) + } + } + + @Test + fun `present - change recovery key and save it`() = runTest { + val encryptionService = FakeEncryptionService() + val presenter = createSecureBackupSetupPresenter( + isChangeRecoveryKeyUserStory = true, + encryptionService = encryptionService + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey) + val creatingState = awaitItem() + assertThat(creatingState.setupState).isEqualTo(SetupState.Creating) + assertThat(creatingState.recoveryKeyViewState).isEqualTo( + RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Change, + formattedRecoveryKey = null, + inProgress = true, + ) + ) + val createdState = awaitItem() + assertThat(createdState.setupState).isEqualTo(SetupState.Created(FakeEncryptionService.fakeRecoveryKey)) + assertThat(createdState.recoveryKeyViewState).isEqualTo( + RecoveryKeyViewState( + recoveryKeyUserStory = RecoveryKeyUserStory.Change, + formattedRecoveryKey = FakeEncryptionService.fakeRecoveryKey, + inProgress = false, + ) + ) + createdState.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved) + val createdAndSaveState = awaitItem() + assertThat(createdAndSaveState.setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java) + createdAndSaveState.eventSink.invoke(SecureBackupSetupEvents.Done) + val doneState = awaitItem() + assertThat(doneState.showSaveConfirmationDialog).isTrue() + doneState.eventSink.invoke(SecureBackupSetupEvents.DismissDialog) + val doneStateCancelled = awaitItem() + assertThat(doneStateCancelled.showSaveConfirmationDialog).isFalse() + } + } + + private fun createSecureBackupSetupPresenter( + isChangeRecoveryKeyUserStory: Boolean = false, + encryptionService: EncryptionService = FakeEncryptionService(), + ): SecureBackupSetupPresenter { + return SecureBackupSetupPresenter( + isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory, + stateMachine = SecureBackupSetupStateMachine(), + encryptionService = encryptionService, + ) + } +} diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyVisualTransformationTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyVisualTransformationTest.kt new file mode 100644 index 0000000000..cfe2ba32a6 --- /dev/null +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/tools/RecoveryKeyVisualTransformationTest.kt @@ -0,0 +1,74 @@ +/* + * 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.securebackup.impl.tools + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class RecoveryKeyVisualTransformationTest { + @Test + fun `RecoveryKeyOffsetMapping computes correct originalToTransformed values`() { + var sut = RecoveryKeyVisualTransformation.RecoveryKeyOffsetMapping("") + assertThat(sut.originalToTransformed(0)).isEqualTo(0) + + sut = RecoveryKeyVisualTransformation.RecoveryKeyOffsetMapping("a") + assertThat(sut.originalToTransformed(0)).isEqualTo(0) + assertThat(sut.originalToTransformed(1)).isEqualTo(1) + + sut = RecoveryKeyVisualTransformation.RecoveryKeyOffsetMapping("ab") + assertThat(sut.originalToTransformed(0)).isEqualTo(0) + assertThat(sut.originalToTransformed(1)).isEqualTo(1) + assertThat(sut.originalToTransformed(2)).isEqualTo(2) + + sut = RecoveryKeyVisualTransformation.RecoveryKeyOffsetMapping("abc") + assertThat(sut.originalToTransformed(0)).isEqualTo(0) + assertThat(sut.originalToTransformed(1)).isEqualTo(1) + assertThat(sut.originalToTransformed(2)).isEqualTo(2) + assertThat(sut.originalToTransformed(3)).isEqualTo(3) + + sut = RecoveryKeyVisualTransformation.RecoveryKeyOffsetMapping("abcd") + assertThat(sut.originalToTransformed(0)).isEqualTo(0) + assertThat(sut.originalToTransformed(1)).isEqualTo(1) + assertThat(sut.originalToTransformed(2)).isEqualTo(2) + assertThat(sut.originalToTransformed(3)).isEqualTo(3) + assertThat(sut.originalToTransformed(4)).isEqualTo(4) + + sut = RecoveryKeyVisualTransformation.RecoveryKeyOffsetMapping("abcde") + assertThat(sut.originalToTransformed(0)).isEqualTo(0) + assertThat(sut.originalToTransformed(1)).isEqualTo(1) + assertThat(sut.originalToTransformed(2)).isEqualTo(2) + assertThat(sut.originalToTransformed(3)).isEqualTo(3) + assertThat(sut.originalToTransformed(4)).isEqualTo(5) + assertThat(sut.originalToTransformed(5)).isEqualTo(6) + } + + @Test + fun `RecoveryKeyOffsetMapping computes correct transformedToOriginal values`() { + val sut = RecoveryKeyVisualTransformation.RecoveryKeyOffsetMapping("" /* Not used by transformedToOriginal */) + assertThat(sut.transformedToOriginal(0)).isEqualTo(0) + assertThat(sut.transformedToOriginal(1)).isEqualTo(1) + assertThat(sut.transformedToOriginal(2)).isEqualTo(2) + assertThat(sut.transformedToOriginal(3)).isEqualTo(3) + assertThat(sut.transformedToOriginal(4)).isEqualTo(4) + assertThat(sut.transformedToOriginal(5)).isEqualTo(4) + assertThat(sut.transformedToOriginal(6)).isEqualTo(5) + assertThat(sut.transformedToOriginal(7)).isEqualTo(6) + assertThat(sut.transformedToOriginal(8)).isEqualTo(7) + assertThat(sut.transformedToOriginal(9)).isEqualTo(8) + assertThat(sut.transformedToOriginal(10)).isEqualTo(8) + } +} diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts index 57e4ca3569..87382a2c3e 100644 --- a/libraries/androidutils/build.gradle.kts +++ b/libraries/androidutils/build.gradle.kts @@ -40,5 +40,5 @@ dependencies { implementation(libs.androidx.recyclerview) implementation(libs.androidx.exifinterface) implementation(libs.androidx.security.crypto) - implementation(libs.androidx.browser) + api(libs.androidx.browser) } 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 e0b8ea7926..20146e6e24 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 @@ -31,10 +31,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +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.ElementThemedPreview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text @@ -52,16 +55,18 @@ fun PreferenceText( modifier: Modifier = Modifier, enabled: Boolean = true, subtitle: String? = null, + subtitleAnnotated: AnnotatedString? = null, currentValue: String? = null, loadingCurrentValue: Boolean = false, icon: ImageVector? = null, @DrawableRes iconResourceId: Int? = null, showIconAreaIfNoIcon: Boolean = false, showIconBadge: Boolean = false, + showEndBadge: Boolean = false, tintColor: Color? = null, onClick: () -> Unit = {}, ) { - val minHeight = if (subtitle == null) preferenceMinHeightOnlyTitle else preferenceMinHeight + val minHeight = if (subtitle == null && subtitleAnnotated == null) preferenceMinHeightOnlyTitle else preferenceMinHeight Row( modifier = modifier @@ -95,6 +100,12 @@ fun PreferenceText( text = subtitle, color = tintColor ?: enabled.toSecondaryEnabledColor(), ) + } else if (subtitleAnnotated != null) { + Text( + style = ElementTheme.typography.fontBodyMdRegular, + text = subtitleAnnotated, + color = tintColor ?: enabled.toSecondaryEnabledColor(), + ) } } if (currentValue != null) { @@ -116,32 +127,63 @@ fun PreferenceText( strokeWidth = 2.dp ) } + if (showEndBadge) { + val endBadgeStartPadding = if (currentValue != null || loadingCurrentValue) 8.dp else 16.dp + RedIndicatorAtom( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = endBadgeStartPadding) + ) + } } } @Preview(group = PreviewGroup.Preferences) @Composable -internal fun PreferenceTextPreview() = ElementThemedPreview { ContentToPreview() } +internal fun PreferenceTextLightPreview() = ElementPreviewLight { + ContentToPreview(showEndBadge = false) +} + +@Preview(group = PreviewGroup.Preferences) +@Composable +internal fun PreferenceTextDarkPreview() = ElementPreviewDark { + ContentToPreview(showEndBadge = false) +} + +@Preview(group = PreviewGroup.Preferences) +@Composable +internal fun PreferenceTextWithEndBadgeLightPreview() = ElementPreviewLight { + ContentToPreview(showEndBadge = true) +} + +@Preview(group = PreviewGroup.Preferences) +@Composable +internal fun PreferenceTextWithEndBadgeDarkPreview() = ElementPreviewDark { + ContentToPreview(showEndBadge = true) +} @Composable -private fun ContentToPreview() { +private fun ContentToPreview(showEndBadge: Boolean) { Column( verticalArrangement = Arrangement.spacedBy(2.dp) ) { PreferenceText( title = "Title", iconResourceId = CommonDrawables.ic_compound_chat_problem, + showEndBadge = showEndBadge, ) PreferenceText( title = "Title", subtitle = "Some content", iconResourceId = CommonDrawables.ic_compound_chat_problem, + showEndBadge = showEndBadge, ) PreferenceText( title = "Title", subtitle = "Some content", iconResourceId = CommonDrawables.ic_compound_chat_problem, currentValue = "123", + showEndBadge = showEndBadge, ) PreferenceText( title = "Title", @@ -149,31 +191,37 @@ private fun ContentToPreview() { iconResourceId = CommonDrawables.ic_compound_chat_problem, currentValue = "123", enabled = false, + showEndBadge = showEndBadge, ) PreferenceText( title = "Title", subtitle = "Some content", iconResourceId = CommonDrawables.ic_compound_chat_problem, loadingCurrentValue = true, + showEndBadge = showEndBadge, ) PreferenceText( title = "Title", iconResourceId = CommonDrawables.ic_compound_chat_problem, currentValue = "123", + showEndBadge = showEndBadge, ) PreferenceText( title = "Title", iconResourceId = CommonDrawables.ic_compound_chat_problem, loadingCurrentValue = true, + showEndBadge = showEndBadge, ) PreferenceText( title = "Title no icon with icon area", showIconAreaIfNoIcon = true, loadingCurrentValue = true, + showEndBadge = showEndBadge, ) PreferenceText( title = "Title no icon", loadingCurrentValue = true, + showEndBadge = showEndBadge, ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Clickable.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Clickable.kt new file mode 100644 index 0000000000..2a74ee6fa9 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Clickable.kt @@ -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. + */ + +package io.element.android.libraries.designsystem.modifiers + +import androidx.compose.foundation.clickable +import androidx.compose.ui.Modifier + +fun Modifier.clickableIfNotNull(onClick: (() -> Unit)? = null): Modifier = then( + if (onClick != null) { + Modifier.clickable { onClick() } + } else { + Modifier + } +) diff --git a/libraries/designsystem/src/main/res/drawable/ic_key.xml b/libraries/designsystem/src/main/res/drawable/ic_key.xml new file mode 100644 index 0000000000..b863406d24 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_key.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_key_filled.xml b/libraries/designsystem/src/main/res/drawable/ic_key_filled.xml new file mode 100644 index 0000000000..d1bee744f7 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_key_filled.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_key_off.xml b/libraries/designsystem/src/main/res/drawable/ic_key_off.xml new file mode 100644 index 0000000000..91ec467dce --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_key_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceStateProvider.kt b/libraries/indicator/api/build.gradle.kts similarity index 73% rename from features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceStateProvider.kt rename to libraries/indicator/api/build.gradle.kts index 3e729298b0..3dd6fae101 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/LogoutPreferenceStateProvider.kt +++ b/libraries/indicator/api/build.gradle.kts @@ -14,11 +14,14 @@ * limitations under the License. */ -package io.element.android.features.logout.api +plugins { + id("io.element.android-compose-library") +} -import io.element.android.libraries.architecture.Async +android { + namespace = "io.element.android.libraries.indicator.api" +} -fun aLogoutPreferenceState() = LogoutPreferenceState( - logoutAction = Async.Uninitialized, - eventSink = {} -) +dependencies { + implementation(libs.coroutines.core) +} diff --git a/libraries/indicator/api/src/main/kotlin/io/element/android/libraries/indicator/api/IndicatorService.kt b/libraries/indicator/api/src/main/kotlin/io/element/android/libraries/indicator/api/IndicatorService.kt new file mode 100644 index 0000000000..27653a42ae --- /dev/null +++ b/libraries/indicator/api/src/main/kotlin/io/element/android/libraries/indicator/api/IndicatorService.kt @@ -0,0 +1,31 @@ +/* + * 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.indicator.api + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State + +/** + * A set of State to observe to display or not the indicators in the UI. + */ +interface IndicatorService { + @Composable + fun showRoomListTopBarIndicator(): State + + @Composable + fun showSettingChatBackupIndicator(): State +} diff --git a/libraries/indicator/impl/build.gradle.kts b/libraries/indicator/impl/build.gradle.kts new file mode 100644 index 0000000000..cb7c6cf9f0 --- /dev/null +++ b/libraries/indicator/impl/build.gradle.kts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) +} + +anvil { + generateDaggerFactories.set(true) +} + +android { + namespace = "io.element.android.libraries.indicator.impl" +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(libs.dagger) + implementation(projects.libraries.di) + implementation(projects.libraries.matrix.api) + implementation(projects.anvilannotations) + + implementation(libs.coroutines.core) + + api(projects.libraries.indicator.api) + + testImplementation(projects.libraries.matrix.test) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.test.turbine) + testImplementation(libs.test.truth) +} diff --git a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt new file mode 100644 index 0000000000..96ec986534 --- /dev/null +++ b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt @@ -0,0 +1,70 @@ +/* + * 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.indicator.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.indicator.api.IndicatorService +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.encryption.RecoveryState +import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultIndicatorService @Inject constructor( + private val sessionVerificationService: SessionVerificationService, + private val encryptionService: EncryptionService, +) : IndicatorService { + + @Composable + override fun showRoomListTopBarIndicator(): State { + val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) + val settingChatBackupIndicator = showSettingChatBackupIndicator() + + return remember { + derivedStateOf { + !canVerifySession && settingChatBackupIndicator.value + } + } + } + + @Composable + override fun showSettingChatBackupIndicator(): State { + val backupState by encryptionService.backupStateStateFlow.collectAsState() + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + + return remember { + derivedStateOf { + val showForBackup = backupState in listOf( + BackupState.UNKNOWN, + ) + val showForRecovery = recoveryState in listOf( + RecoveryState.DISABLED, + RecoveryState.INCOMPLETE, + ) + showForBackup || showForRecovery + } + } + } +} 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 e5f46af0e6..689f88cdee 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -245,6 +245,7 @@ Pokud budete pokračovat, některá nastavení se mohou změnit." "en" "Chyba" "Úspěch" + "Všichni" "Sdílejte anonymní údaje o používání, které nám pomohou identifikovat problémy." "Můžete si přečíst všechny naše podmínky %1$s." "zde" 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 64d233e598..db0c685397 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -245,6 +245,7 @@ "en" "Ошибка" "Успешно" + "Для всех" "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы." "Вы можете ознакомиться со всеми нашими условиями %1$s." "здесь" 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 ccefa660f2..b24831294a 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -218,22 +218,6 @@ "Toto je začiatok tejto konverzácie." "Nové" "Zdieľať analytické údaje" - "Vypnúť zálohovanie" - "Zapnúť zálohovanie" - "Zálohovanie zaisťuje, že nestratíte históriu správ. %1$s." - "Zálohovanie" - "Zmeniť kľúč na obnovenie" - "Potvrdiť kľúč na obnovenie" - "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í." - "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?" - "Vypnutím zálohovania sa odstráni aktuálna záloha šifrovacích kľúčov a vypnú sa ďalšie bezpečnostné funkcie. V tomto prípade:" - "Na nových zariadeniach nebudete mať zašifrovanú históriu správ" - "Stratíte prístup k svojim zašifrovaným správam, ak sa odhlásite zo všetkých %1$s zariadení" - "Ste si istí, že chcete vypnúť zálohovanie?" "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." @@ -264,29 +248,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" - "Získajte nový kľúč na obnovenie, ak ste stratili svoj existujúci. Po zmene kľúča na obnovenie už starý kľúč nebude fungovať." - "Vygenerovať nový kľúč na obnovenie" - "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." - "Zadajte 48-znakový kód." - "Zadať…" - "Kľúč na obnovu potvrdený" - "Potvrďte kľúč na obnovenie" - "Skopírovaný kľúč na obnovenie" - "Generovanie…" - "Uložiť kľúč na obnovenie" - "Zapíšte si kľúč na obnovenie na bezpečné miesto alebo ho uložte do správcu hesiel." - "Ťuknutím skopírujte kľúč na obnovenie" - "Uložte svoj kľúč na obnovenie" - "Po tomto kroku nebudete mať prístup k novému kľúču na obnovenie." - "Uložili ste kľúč na obnovenie?" - "Vaša záloha konverzácie je chránená kľúčom na obnovenie. Ak potrebujete nový kľúč na obnovenie, po nastavení si ho môžete znova vytvoriť výberom položky „Zmeniť kľúč na obnovenie“." - "Vygenerujte si váš kľúč na obnovenie" - "Uistite sa, že kľúč na obnovenie môžete uložiť niekde v bezpečí" - "Úspešné nastavenie obnovy" - "Nastaviť obnovenie" "Označte, či chcete skryť všetky aktuálne a budúce správy od tohto používateľa" "Zdieľať polohu" "Zdieľať moju polohu" @@ -301,6 +262,7 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""sk" "Chyba" "Úspech" + "Všetci" "Zdieľajte anonymné údaje o používaní, aby sme mohli identifikovať problémy." "Môžete si prečítať všetky naše podmienky %1$s." "tu" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 5a5815c602..8ff9fc54b9 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -136,6 +136,7 @@ "Report a bug" "Report submitted" "Rich text editor" + "Room" "Room name" "e.g. your project name" "Screen lock" @@ -173,8 +174,6 @@ "Waiting for decryption key" "Are you sure you want to end this poll?" "Poll: %1$s" - "Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup." - "Confirm your recovery key" "Confirmation" "Warning" "Activities" @@ -219,23 +218,8 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "Notify the whole room" "Share analytics data" - "Turn off backup" - "Turn on backup" - "Backup ensures that you don\'t lose your message history. %1$s." - "Backup" - "Change recovery key" - "Confirm recovery key" - "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." - "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?" - "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:" - "Not have encrypted message history on new devices" - "Lose access to your encrypted messages if you are signed out of %1$s everywhere" - "Are you sure you want to turn off backup?" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." @@ -264,29 +248,6 @@ If you proceed, some of your settings may change." "system settings" "System notifications turned off" "Notifications" - "Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work." - "Generate a new recovery key" - "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." - "Enter the 48 character code." - "Enter…" - "Recovery key confirmed" - "Confirm your recovery key" - "Copied recovery key" - "Generating…" - "Save recovery key" - "Write down your recovery key somewhere safe or save it in a password manager." - "Tap to copy recovery key" - "Save your recovery key" - "You will not be able to access your new recovery key after this step." - "Have you saved your recovery key?" - "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’." - "Generate your recovery key" - "Make sure you can store your recovery key somewhere safe" - "Recovery setup successful" - "Set up recovery" "Check if you want to hide all current and future messages from this user" "Share location" "Share my location" @@ -302,6 +263,7 @@ If you proceed, some of your settings may change." "en" "Error" "Success" + "Everyone" "Share anonymous usage data to help us identify issues." "You can read all our terms %1$s." "here" diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 28069b932c..c6786ef8b3 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -83,6 +83,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:network")) implementation(project(":libraries:core")) implementation(project(":libraries:eventformatter:impl")) + implementation(project(":libraries:indicator:impl")) implementation(project(":libraries:permissions:impl")) implementation(project(":libraries:push:impl")) implementation(project(":libraries:push:impl")) diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 016989a2d6..150a718e44 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.libraries.network) implementation(projects.libraries.dateformatter.impl) implementation(projects.libraries.eventformatter.impl) + implementation(projects.libraries.indicator.impl) implementation(projects.features.invitelist.impl) implementation(projects.features.roomlist.impl) implementation(projects.features.leaveroom.impl) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 0c43619f39..b43e7d940a 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 @@ -36,6 +36,7 @@ import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFo import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter import io.element.android.libraries.eventformatter.impl.StateContentFormatter +import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -59,6 +60,7 @@ class RoomListScreen( private val dateTimeProvider = LocalDateTimeProvider(clock, timeZone) private val dateFormatters = DateFormatters(locale, clock, timeZone) private val sessionVerificationService = matrixClient.sessionVerificationService() + private val encryptionService = matrixClient.encryptionService() private val stringProvider = AndroidStringProvider(context.resources) private val presenter = RoomListPresenter( client = matrixClient, @@ -80,6 +82,11 @@ class RoomListScreen( coroutineDispatchers = coroutineDispatchers, notificationSettingsService = matrixClient.notificationSettingsService(), appScope = Singleton.appScope + ), + encryptionService = encryptionService, + indicatorService = DefaultIndicatorService( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, ) ) diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt index 32fd6dd624..5f7acf8d39 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistTestTest.kt @@ -40,7 +40,9 @@ class KonsistTestTest { .functions() .withReturnType { it.name.endsWith("Presenter") } .withoutOverrideModifier() - .assertTrue { functionDeclaration -> + .assertTrue( + additionalMessage = "The function can also be named 'createPresenter'. To please Konsist in this case, just remove the return type." + ) { functionDeclaration -> functionDeclaration.name == "create${functionDeclaration.returnType?.name}" } } diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 5144dc191d..6b7f5202d8 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -14,7 +14,7 @@ ] }, { - "name": ":features:logout:api", + "name": ":features:logout:impl", "includeRegex": [ "screen_signout_.*" ] @@ -96,7 +96,8 @@ "name": ":features:roomlist:impl", "includeRegex": [ "screen_roomlist_.*", - "session_verification_banner_.*" + "session_verification_banner_.*", + "confirm_recovery_key_banner_.*" ] }, { @@ -141,6 +142,14 @@ "screen_create_poll_.*" ] }, + { + "name": ":features:securebackup:impl", + "includeRegex": [ + "screen_chat_backup_.*", + "screen_key_backup_disable_.*", + "screen_recovery_key_.*" + ] + }, { "name": ":features:preferences:impl", "includeRegex": [ From 44de6adb86d83104a25a3b4ece02e75771173245 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 27 Oct 2023 12:26:40 +0200 Subject: [PATCH 213/281] Initial support for member suggestions (#1631) * Initial support for member suggestion (search and UI) * Add custom `BottomSheetScaffold` implementation to workaround several scrolling bugs * Start searching as soon as `@` is typed, add UI following initial designs * Extract suggestion processing code * Extract component, add previews, fix tests * Add tests * Add exception from kover to the forked bottom sheet code * Add a feature flag for mentions - Extract composer & mention suggestions to their composable. - Extract mentions suggestions processing to its own class. - Add `MatrixRoom.canTriggerRoomNotification` function. - Update strings and conditions for displaying the `@room` mention. --------- Co-authored-by: ElementBot --- build.gradle.kts | 4 +- changelog.d/1452.feature | 1 + .../location/impl/send/SendLocationView.kt | 8 +- .../impl/ExpandableBottomSheetScaffold.kt | 13 +- .../features/messages/impl/MessagesView.kt | 70 ++- .../mentions/MentionSuggestionsPickerView.kt | 169 ++++++ .../mentions/MentionSuggestionsProcessor.kt | 111 ++++ .../messagecomposer/MessageComposerEvents.kt | 2 + .../MessageComposerPresenter.kt | 58 ++ .../messagecomposer/MessageComposerState.kt | 1 + .../MessageComposerStateProvider.kt | 4 + .../messagecomposer/MessageComposerView.kt | 10 +- .../impl/src/main/res/values/localazy.xml | 2 + .../messages/MessagesPresenterTest.kt | 3 + .../MessageComposerPresenterTest.kt | 113 ++++ .../theme/components/BottomSheetScaffold.kt | 7 +- .../bottomsheet/CustomBottomSheetScaffold.kt | 519 ++++++++++++++++++ .../bottomsheet/CustomSheetState.kt | 309 +++++++++++ .../libraries/featureflag/api/FeatureFlags.kt | 6 + .../impl/StaticFeatureFlagProvider.kt | 1 + .../libraries/matrix/api/room/MatrixRoom.kt | 2 + .../matrix/api/user/CurrentSessionIdHolder.kt | 30 + .../matrix/impl/room/RustMatrixRoom.kt | 6 + .../matrix/test/room/FakeMatrixRoom.kt | 10 + .../libraries/textcomposer/TextComposer.kt | 56 +- .../textcomposer/model/Suggestion.kt | 50 ++ .../tests/konsist/KonsistArchitectureTest.kt | 1 + ...PickerView_-D-3_3_null,NEXUS_5,1.0,en].png | 3 + ...PickerView_-N-3_4_null,NEXUS_5,1.0,en].png | 3 + ...PickerMenu-D-4_4_null,NEXUS_5,1.0,en].png} | 0 ...PickerMenu-N-4_5_null,NEXUS_5,1.0,en].png} | 0 ...oserView-D-5_5_null_0,NEXUS_5,1.0,en].png} | 0 ...oserView-N-5_6_null_0,NEXUS_5,1.0,en].png} | 0 ...iewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png} | 0 ...iewVoice-N-6_7_null_0,NEXUS_5,1.0,en].png} | 0 ...sageView-D-7_7_null_0,NEXUS_5,1.0,en].png} | 0 ...sageView-D-7_7_null_1,NEXUS_5,1.0,en].png} | 0 ...sageView-D-7_7_null_2,NEXUS_5,1.0,en].png} | 0 ...sageView-D-7_7_null_3,NEXUS_5,1.0,en].png} | 0 ...sageView-D-7_7_null_4,NEXUS_5,1.0,en].png} | 0 ...sageView-D-7_7_null_5,NEXUS_5,1.0,en].png} | 0 ...sageView-N-7_8_null_0,NEXUS_5,1.0,en].png} | 0 ...sageView-N-7_8_null_1,NEXUS_5,1.0,en].png} | 0 ...sageView-N-7_8_null_2,NEXUS_5,1.0,en].png} | 0 ...sageView-N-7_8_null_3,NEXUS_5,1.0,en].png} | 0 ...sageView-N-7_8_null_4,NEXUS_5,1.0,en].png} | 0 ...sageView-N-7_8_null_5,NEXUS_5,1.0,en].png} | 0 ...mojiItem-D-26_26_null,NEXUS_5,1.0,en].png} | 0 ...mojiItem-N-26_27_null,NEXUS_5,1.0,en].png} | 0 ...jiPicker-D-27_27_null,NEXUS_5,1.0,en].png} | 0 ...jiPicker-N-27_28_null,NEXUS_5,1.0,en].png} | 0 ...ioView-D-28_28_null_0,NEXUS_5,1.0,en].png} | 0 ...ioView-D-28_28_null_1,NEXUS_5,1.0,en].png} | 0 ...ioView-D-28_28_null_2,NEXUS_5,1.0,en].png} | 0 ...ioView-N-28_29_null_0,NEXUS_5,1.0,en].png} | 0 ...ioView-N-28_29_null_1,NEXUS_5,1.0,en].png} | 0 ...ioView-N-28_29_null_2,NEXUS_5,1.0,en].png} | 0 ...ptedView-D-29_29_null,NEXUS_5,1.0,en].png} | 0 ...ptedView-N-29_30_null,NEXUS_5,1.0,en].png} | 0 ...leView-D-30_30_null_0,NEXUS_5,1.0,en].png} | 0 ...leView-D-30_30_null_1,NEXUS_5,1.0,en].png} | 0 ...leView-D-30_30_null_2,NEXUS_5,1.0,en].png} | 0 ...leView-N-30_31_null_0,NEXUS_5,1.0,en].png} | 0 ...leView-N-30_31_null_1,NEXUS_5,1.0,en].png} | 0 ...leView-N-30_31_null_2,NEXUS_5,1.0,en].png} | 0 ...geView-D-31_31_null_0,NEXUS_5,1.0,en].png} | 0 ...geView-D-31_31_null_1,NEXUS_5,1.0,en].png} | 0 ...geView-D-31_31_null_2,NEXUS_5,1.0,en].png} | 0 ...geView-N-31_32_null_0,NEXUS_5,1.0,en].png} | 0 ...geView-N-31_32_null_1,NEXUS_5,1.0,en].png} | 0 ...geView-N-31_32_null_2,NEXUS_5,1.0,en].png} | 0 ...tiveView-D-32_32_null,NEXUS_5,1.0,en].png} | 0 ...tiveView-N-32_33_null,NEXUS_5,1.0,en].png} | 0 ...onView-D-33_33_null_0,NEXUS_5,1.0,en].png} | 0 ...onView-D-33_33_null_1,NEXUS_5,1.0,en].png} | 0 ...onView-N-33_34_null_0,NEXUS_5,1.0,en].png} | 0 ...onView-N-33_34_null_1,NEXUS_5,1.0,en].png} | 0 ...orView-D-35_35_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-D-35_35_null_1,NEXUS_5,1.0,en].png} | 0 ...orView-N-35_36_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-N-35_36_null_1,NEXUS_5,1.0,en].png} | 0 ...llView-D-34_34_null_0,NEXUS_5,1.0,en].png} | 0 ...llView-D-34_34_null_1,NEXUS_5,1.0,en].png} | 0 ...llView-N-34_35_null_0,NEXUS_5,1.0,en].png} | 0 ...llView-N-34_35_null_1,NEXUS_5,1.0,en].png} | 0 ...ctedView-D-36_36_null,NEXUS_5,1.0,en].png} | 0 ...ctedView-N-36_37_null,NEXUS_5,1.0,en].png} | 0 ...tateView-D-37_37_null,NEXUS_5,1.0,en].png} | 0 ...tateView-N-37_38_null,NEXUS_5,1.0,en].png} | 0 ...xtView-D-38_38_null_0,NEXUS_5,1.0,en].png} | 0 ...xtView-D-38_38_null_1,NEXUS_5,1.0,en].png} | 0 ...xtView-D-38_38_null_2,NEXUS_5,1.0,en].png} | 0 ...xtView-D-38_38_null_3,NEXUS_5,1.0,en].png} | 0 ...xtView-D-38_38_null_4,NEXUS_5,1.0,en].png} | 0 ...xtView-D-38_38_null_5,NEXUS_5,1.0,en].png} | 0 ...xtView-N-38_39_null_0,NEXUS_5,1.0,en].png} | 0 ...xtView-N-38_39_null_1,NEXUS_5,1.0,en].png} | 0 ...xtView-N-38_39_null_2,NEXUS_5,1.0,en].png} | 0 ...xtView-N-38_39_null_3,NEXUS_5,1.0,en].png} | 0 ...xtView-N-38_39_null_4,NEXUS_5,1.0,en].png} | 0 ...xtView-N-38_39_null_5,NEXUS_5,1.0,en].png} | 0 ...nownView-D-39_39_null,NEXUS_5,1.0,en].png} | 0 ...nownView-N-39_40_null,NEXUS_5,1.0,en].png} | 0 ...eoView-D-40_40_null_0,NEXUS_5,1.0,en].png} | 0 ...eoView-D-40_40_null_1,NEXUS_5,1.0,en].png} | 0 ...eoView-D-40_40_null_2,NEXUS_5,1.0,en].png} | 0 ...eoView-N-40_41_null_0,NEXUS_5,1.0,en].png} | 0 ...eoView-N-40_41_null_1,NEXUS_5,1.0,en].png} | 0 ...eoView-N-40_41_null_2,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_0,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_1,NEXUS_5,1.0,en].png} | 0 ...eView-D-41_41_null_10,NEXUS_5,1.0,en].png} | 0 ...eView-D-41_41_null_11,NEXUS_5,1.0,en].png} | 0 ...eView-D-41_41_null_12,NEXUS_5,1.0,en].png} | 0 ...eView-D-41_41_null_13,NEXUS_5,1.0,en].png} | 0 ...eView-D-41_41_null_14,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_2,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_3,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_4,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_5,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_6,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_7,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_8,NEXUS_5,1.0,en].png} | 0 ...ceView-D-41_41_null_9,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_0,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_1,NEXUS_5,1.0,en].png} | 0 ...eView-N-41_42_null_10,NEXUS_5,1.0,en].png} | 0 ...eView-N-41_42_null_11,NEXUS_5,1.0,en].png} | 0 ...eView-N-41_42_null_12,NEXUS_5,1.0,en].png} | 0 ...eView-N-41_42_null_13,NEXUS_5,1.0,en].png} | 0 ...eView-N-41_42_null_14,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_2,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_3,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_4,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_5,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_6,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_7,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_8,NEXUS_5,1.0,en].png} | 0 ...ceView-N-41_42_null_9,NEXUS_5,1.0,en].png} | 0 ...wUnified-D-42_42_null,NEXUS_5,1.0,en].png} | 0 ...wUnified-N-42_43_null,NEXUS_5,1.0,en].png} | 0 ...aderView-D-43_43_null,NEXUS_5,1.0,en].png} | 0 ...aderView-N-43_44_null,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_0,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_1,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_10,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_11,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_12,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_13,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_14,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_15,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_16,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_17,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_18,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_19,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_2,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_20,NEXUS_5,1.0,en].png} | 0 ...ument-D-44_44_null_21,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_3,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_4,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_5,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_6,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_7,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_8,NEXUS_5,1.0,en].png} | 0 ...cument-D-44_44_null_9,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_0,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_1,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_10,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_11,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_12,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_13,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_14,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_15,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_16,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_17,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_18,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_19,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_2,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_20,NEXUS_5,1.0,en].png} | 0 ...ument-N-44_45_null_21,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_3,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_4,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_5,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_6,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_7,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_8,NEXUS_5,1.0,en].png} | 0 ...cument-N-44_45_null_9,NEXUS_5,1.0,en].png} | 0 ...ontent-D-45_45_null_0,NEXUS_5,1.0,en].png} | 0 ...ontent-N-45_46_null_0,NEXUS_5,1.0,en].png} | 0 ...geMenu-D-46_46_null_0,NEXUS_5,1.0,en].png} | 0 ...geMenu-D-46_46_null_1,NEXUS_5,1.0,en].png} | 0 ...geMenu-N-46_47_null_0,NEXUS_5,1.0,en].png} | 0 ...geMenu-N-46_47_null_1,NEXUS_5,1.0,en].png} | 0 ...nnerView-D-47_47_null,NEXUS_5,1.0,en].png} | 0 ...nnerView-N-47_48_null,NEXUS_5,1.0,en].png} | 0 ...orView-D-48_48_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-D-48_48_null_1,NEXUS_5,1.0,en].png} | 0 ...orView-N-48_49_null_0,NEXUS_5,1.0,en].png} | 0 ...orView-N-48_49_null_1,NEXUS_5,1.0,en].png} | 0 ...ndicator-D-49_49_null,NEXUS_5,1.0,en].png} | 0 ...ndicator-N-49_50_null,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_0,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_1,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-9_9_null_10,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-9_9_null_11,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-9_9_null_12,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-9_9_null_13,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-9_9_null_14,NEXUS_5,1.0,en].png} | 0 ...tBubble-D-9_9_null_15,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_2,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_3,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_4,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_5,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_6,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_7,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_8,NEXUS_5,1.0,en].png} | 0 ...ntBubble-D-9_9_null_9,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_0,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_1,NEXUS_5,1.0,en].png} | 0 ...Bubble-N-9_10_null_10,NEXUS_5,1.0,en].png} | 0 ...Bubble-N-9_10_null_11,NEXUS_5,1.0,en].png} | 0 ...Bubble-N-9_10_null_12,NEXUS_5,1.0,en].png} | 0 ...Bubble-N-9_10_null_13,NEXUS_5,1.0,en].png} | 0 ...Bubble-N-9_10_null_14,NEXUS_5,1.0,en].png} | 0 ...Bubble-N-9_10_null_15,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_2,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_3,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_4,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_5,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_6,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_7,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_8,NEXUS_5,1.0,en].png} | 0 ...tBubble-N-9_10_null_9,NEXUS_5,1.0,en].png} | 0 ...ontainer-D-10_10_null,NEXUS_5,1.0,en].png} | 0 ...ontainer-N-10_11_null,NEXUS_5,1.0,en].png} | 0 ...onButton-D-12_12_null,NEXUS_5,1.0,en].png} | 0 ...onButton-N-12_13_null,NEXUS_5,1.0,en].png} | 0 ...Button-D-11_11_null_0,NEXUS_5,1.0,en].png} | 0 ...Button-D-11_11_null_1,NEXUS_5,1.0,en].png} | 0 ...Button-D-11_11_null_2,NEXUS_5,1.0,en].png} | 0 ...Button-D-11_11_null_3,NEXUS_5,1.0,en].png} | 0 ...Button-N-11_12_null_0,NEXUS_5,1.0,en].png} | 0 ...Button-N-11_12_null_1,NEXUS_5,1.0,en].png} | 0 ...Button-N-11_12_null_2,NEXUS_5,1.0,en].png} | 0 ...Button-N-11_12_null_3,NEXUS_5,1.0,en].png} | 0 ...aButtons-D-13_13_null,NEXUS_5,1.0,en].png} | 0 ...aButtons-N-13_14_null,NEXUS_5,1.0,en].png} | 0 ...ndicator-D-14_14_null,NEXUS_5,1.0,en].png} | 0 ...ndicator-N-14_15_null,NEXUS_5,1.0,en].png} | 0 ...mpView-D-15_15_null_0,NEXUS_5,1.0,en].png} | 0 ...mpView-D-15_15_null_1,NEXUS_5,1.0,en].png} | 0 ...mpView-D-15_15_null_2,NEXUS_5,1.0,en].png} | 0 ...mpView-D-15_15_null_3,NEXUS_5,1.0,en].png} | 0 ...mpView-N-15_16_null_0,NEXUS_5,1.0,en].png} | 0 ...mpView-N-15_16_null_1,NEXUS_5,1.0,en].png} | 0 ...mpView-N-15_16_null_2,NEXUS_5,1.0,en].png} | 0 ...mpView-N-15_16_null_3,NEXUS_5,1.0,en].png} | 0 ...EventRow-D-16_16_null,NEXUS_5,1.0,en].png} | 0 ...EventRow-N-16_17_null,NEXUS_5,1.0,en].png} | 0 ...estamp-D-18_18_null_0,NEXUS_5,1.0,en].png} | 0 ...estamp-D-18_18_null_1,NEXUS_5,1.0,en].png} | 0 ...estamp-D-18_18_null_2,NEXUS_5,1.0,en].png} | 0 ...estamp-D-18_18_null_3,NEXUS_5,1.0,en].png} | 0 ...estamp-N-18_19_null_0,NEXUS_5,1.0,en].png} | 0 ...estamp-N-18_19_null_1,NEXUS_5,1.0,en].png} | 0 ...estamp-N-18_19_null_2,NEXUS_5,1.0,en].png} | 0 ...estamp-N-18_19_null_3,NEXUS_5,1.0,en].png} | 0 ...eactions-D-19_19_null,NEXUS_5,1.0,en].png} | 0 ...eactions-N-19_20_null,NEXUS_5,1.0,en].png} | 0 ...ithReply-D-17_17_null,NEXUS_5,1.0,en].png} | 0 ...ithReply-N-17_18_null,NEXUS_5,1.0,en].png} | 0 ...nsLayout-D-20_20_null,NEXUS_5,1.0,en].png} | 0 ...nsLayout-N-20_21_null,NEXUS_5,1.0,en].png} | 0 ...ionsView-D-21_21_null,NEXUS_5,1.0,en].png} | 0 ...ionsView-N-21_22_null,NEXUS_5,1.0,en].png} | 0 ...sViewFew-D-22_22_null,NEXUS_5,1.0,en].png} | 0 ...sViewFew-N-22_23_null,NEXUS_5,1.0,en].png} | 0 ...Incoming-D-23_23_null,NEXUS_5,1.0,en].png} | 0 ...Incoming-N-23_24_null,NEXUS_5,1.0,en].png} | 0 ...Outgoing-D-24_24_null,NEXUS_5,1.0,en].png} | 0 ...Outgoing-N-24_25_null,NEXUS_5,1.0,en].png} | 0 ...EventRow-D-25_25_null,NEXUS_5,1.0,en].png} | 0 ...EventRow-N-25_26_null,NEXUS_5,1.0,en].png} | 0 ...InfoView-D-50_50_null,NEXUS_5,1.0,en].png} | 0 ...InfoView-N-50_51_null,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_0,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_1,NEXUS_5,1.0,en].png} | 0 ...ineView-D-8_8_null_10,NEXUS_5,1.0,en].png} | 0 ...ineView-D-8_8_null_11,NEXUS_5,1.0,en].png} | 0 ...ineView-D-8_8_null_12,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_2,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_3,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_4,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_5,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_6,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_7,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_8,NEXUS_5,1.0,en].png} | 0 ...lineView-D-8_8_null_9,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_0,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_1,NEXUS_5,1.0,en].png} | 0 ...ineView-N-8_9_null_10,NEXUS_5,1.0,en].png} | 0 ...ineView-N-8_9_null_11,NEXUS_5,1.0,en].png} | 0 ...ineView-N-8_9_null_12,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_2,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_3,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_4,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_5,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_6,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_7,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_8,NEXUS_5,1.0,en].png} | 0 ...lineView-N-8_9_null_9,NEXUS_5,1.0,en].png} | 0 tools/localazy/config.json | 1 + 312 files changed, 1522 insertions(+), 51 deletions(-) create mode 100644 changelog.d/1452.feature create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsProcessor.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Suggestion.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.mentions_null_MentionSuggestionsPickerView_-D-3_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.mentions_null_MentionSuggestionsPickerView_-N-3_4_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-3_3_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-4_4_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-3_4_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-4_5_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-5_5_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-5_6_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-6_7_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-D-25_25_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-D-26_26_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-N-25_26_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-N-26_27_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-26_26_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-27_27_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-26_27_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-27_28_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-27_27_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-27_27_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-27_27_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-27_28_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-27_28_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-27_28_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-28_28_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-29_29_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-28_29_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-29_30_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-29_29_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-29_29_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-29_29_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-29_30_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-29_30_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-29_30_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-30_30_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-30_30_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-30_30_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-30_31_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-30_31_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-30_31_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-31_31_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-32_32_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-31_32_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-32_33_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-32_32_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-33_33_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-32_32_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-33_33_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-32_33_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-33_34_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-32_33_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-33_34_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-34_34_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-35_35_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-34_34_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-35_35_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-34_35_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-35_36_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-34_35_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-35_36_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-33_33_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-34_34_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-33_33_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-34_34_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-33_34_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-34_35_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-33_34_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-34_35_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-35_35_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-36_36_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-35_36_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-36_37_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-36_36_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-37_37_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-36_37_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-37_38_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-38_38_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-39_39_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-38_39_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-39_40_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-39_39_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-39_39_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-39_39_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-39_40_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-39_40_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-39_40_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-42_42_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-42_42_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-43_43_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-42_43_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-43_44_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_16,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_16,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_17,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_17,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_18,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_18,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_19,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_19,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_20,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_20,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_21,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_21,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_16,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_16,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_17,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_17,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_18,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_18,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_19,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_19,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_20,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_20,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_21,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_21,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-44_44_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-45_45_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-44_45_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-45_46_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-45_45_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-46_46_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-45_45_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-46_46_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-45_46_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-46_47_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-45_46_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-46_47_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-47_47_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-48_48_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-47_47_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-48_48_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-47_48_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-48_49_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-47_48_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-48_49_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-48_48_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-49_49_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-48_49_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-49_50_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_13,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_13,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_14,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_14,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_15,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_15,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-9_9_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-10_10_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-9_10_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-10_11_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-11_11_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-12_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-11_12_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-12_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-12_12_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-13_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-12_13_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-13_14_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-13_13_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-14_14_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-13_14_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-14_15_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-15_15_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-16_16_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-15_16_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-16_17_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-18_18_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-19_19_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-18_19_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-19_20_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-16_16_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-17_17_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-16_17_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-17_18_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-19_19_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-20_20_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-19_20_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-20_21_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-20_20_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-21_21_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-20_21_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-21_22_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-21_21_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-22_22_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-21_22_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-22_23_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-22_22_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-23_23_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-22_23_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-23_24_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-23_23_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-24_24_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-23_24_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-24_25_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-24_24_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-25_25_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-24_25_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-25_26_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-49_49_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-50_50_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-49_50_null,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-50_51_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_9,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_10,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_10,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_11,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_11,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_12,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_12,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_8,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_9,NEXUS_5,1.0,en].png => ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_9,NEXUS_5,1.0,en].png} (100%) diff --git a/build.gradle.kts b/build.gradle.kts index 9cbf6cd3cc..598d4aab43 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -200,7 +200,9 @@ koverMerged { "*Node$*", // Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix SDK api, so it is not really relevant to unit test it: there is no logic to test. "io.element.android.libraries.matrix.impl.*", - "*Presenter\$present\$*" + "*Presenter\$present\$*", + // Forked from compose + "io.element.android.libraries.designsystem.theme.components.bottomsheet.*", ) ) } diff --git a/changelog.d/1452.feature b/changelog.d/1452.feature new file mode 100644 index 0000000000..acdee4039e --- /dev/null +++ b/changelog.d/1452.feature @@ -0,0 +1 @@ +Mentions: add mentions suggestion view in RTE diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt index e70fe79fdf..124f30e9f4 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/send/SendLocationView.kt @@ -33,8 +33,6 @@ import androidx.compose.material.icons.filled.MyLocation import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ListItem import androidx.compose.material3.SheetValue -import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -47,19 +45,21 @@ import com.mapbox.mapboxsdk.camera.CameraPosition import io.element.android.features.location.api.Location import io.element.android.features.location.api.internal.centerBottomEdge import io.element.android.features.location.api.internal.rememberTileStyleUrl -import io.element.android.features.location.impl.common.MapDefaults import io.element.android.features.location.impl.R +import io.element.android.features.location.impl.common.MapDefaults import io.element.android.features.location.impl.common.PermissionDeniedDialog import io.element.android.features.location.impl.common.PermissionRationaleDialog import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.BottomSheetScaffold import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState +import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberStandardBottomSheetState import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.maplibre.compose.CameraMode import io.element.android.libraries.maplibre.compose.CameraMoveStartedReason diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt index 8b28a216b0..215559b334 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt @@ -20,12 +20,8 @@ package io.element.android.features.messages.impl import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue -import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -42,6 +38,10 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min +import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold +import io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState +import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState +import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberStandardBottomSheetState import kotlin.math.roundToInt /** @@ -58,6 +58,7 @@ import kotlin.math.roundToInt * @param modifier The modifier for the layout. * @param sheetContentKey The key for the sheet content. If the key changes, the sheet will be remeasured. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ExpandableBottomSheetScaffold( content: @Composable (padding: PaddingValues) -> Unit, @@ -139,7 +140,7 @@ internal fun ExpandableBottomSheetScaffold( modifier = Modifier.fillMaxHeight(), measurePolicy = { measurables, constraints -> val constraintHeight = constraints.maxHeight - val offset = scaffoldState.bottomSheetState.getOffset() ?: 0 + val offset = scaffoldState.bottomSheetState.getIntOffset() ?: 0 val height = Integer.max(0, constraintHeight - offset) val top = measurables[0].measure( constraints.copy( @@ -163,7 +164,7 @@ internal fun ExpandableBottomSheetScaffold( }) } -private fun SheetState.getOffset(): Int? = try { +private fun CustomSheetState.getIntOffset(): Int? = try { requireOffset().roundToInt() } catch (e: IllegalStateException) { null 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 9ecf63dd29..5c08c62849 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 @@ -43,7 +43,11 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle @@ -55,6 +59,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.mentions.MentionSuggestionsPickerView import io.element.android.features.messages.impl.messagecomposer.AttachmentsBottomSheet import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents @@ -338,7 +343,11 @@ private fun MessagesViewContent( @Composable {} }, sheetSwipeEnabled = state.composerState.showTextFormatting, - sheetShape = if (state.composerState.showTextFormatting) MaterialTheme.shapes.large else RectangleShape, + sheetShape = if (state.composerState.showTextFormatting || state.composerState.memberSuggestions.isNotEmpty()) { + MaterialTheme.shapes.large + } else { + RectangleShape + }, content = { paddingValues -> TimelineView( modifier = Modifier.padding(paddingValues), @@ -354,27 +363,56 @@ private fun MessagesViewContent( ) }, sheetContent = { subcomposing: Boolean -> - if (state.userHasPermissionToSendMessage) { - MessageComposerView( - state = state.composerState, - voiceMessageState = state.voiceMessageComposerState, - subcomposing = subcomposing, - enableTextFormatting = state.enableTextFormatting, - enableVoiceMessages = state.enableVoiceMessages, - modifier = Modifier - .fillMaxWidth(), - ) - } else { - CantSendMessageBanner() - } + MessagesViewComposerBottomSheetContents( + subcomposing = subcomposing, + state = state, + ) }, - sheetContentKey = state.composerState.richTextEditorState.lineCount, + sheetContentKey = state.composerState.richTextEditorState.lineCount + state.composerState.memberSuggestions.size, sheetTonalElevation = 0.dp, - sheetShadowElevation = 0.dp, + sheetShadowElevation = if (state.composerState.memberSuggestions.isNotEmpty()) 16.dp else 0.dp, ) } } +@Composable +private fun MessagesViewComposerBottomSheetContents( + subcomposing: Boolean, + state: MessagesState, + modifier: Modifier = Modifier, +) { + if (state.userHasPermissionToSendMessage) { + Column(modifier = modifier.fillMaxWidth()) { + MentionSuggestionsPickerView( + modifier = Modifier.heightIn(max = 230.dp) + // Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions + .nestedScroll(object : NestedScrollConnection { + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + return available + } + }), + roomId = state.roomId, + roomName = state.roomName.dataOrNull(), + roomAvatarData = state.roomAvatar.dataOrNull(), + memberSuggestions = state.composerState.memberSuggestions, + onSuggestionSelected = { + // TODO pass the selected suggestion to the RTE so it can be inserted as a pill + } + ) + MessageComposerView( + state = state.composerState, + voiceMessageState = state.voiceMessageComposerState, + subcomposing = subcomposing, + enableTextFormatting = state.enableTextFormatting, + enableVoiceMessages = state.enableVoiceMessages, + modifier = Modifier.fillMaxWidth(), + ) + } + } else { + CantSendMessageBanner(modifier = modifier) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun MessagesViewTopBar( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt new file mode 100644 index 0000000000..6b937a872e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt @@ -0,0 +1,169 @@ +/* + * 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.mentions + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.messagecomposer.RoomMemberSuggestion +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.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.Text +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.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun MentionSuggestionsPickerView( + roomId: RoomId, + roomName: String?, + roomAvatarData: AvatarData?, + memberSuggestions: ImmutableList, + onSuggestionSelected: (RoomMemberSuggestion) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier.fillMaxWidth(), + ) { + items( + memberSuggestions, + key = { suggestion -> + when (suggestion) { + is RoomMemberSuggestion.Room -> "@room" + is RoomMemberSuggestion.Member -> suggestion.roomMember.userId.value + } + } + ) { + Column(modifier = Modifier.fillParentMaxWidth()) { + RoomMemberSuggestionItemView( + memberSuggestion = it, + roomId = roomId.value, + roomName = roomName, + roomAvatar = roomAvatarData, + onSuggestionSelected = onSuggestionSelected, + modifier = Modifier.fillMaxWidth() + ) + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + } + } +} + +@Composable +private fun RoomMemberSuggestionItemView( + memberSuggestion: RoomMemberSuggestion, + roomId: String, + roomName: String?, + roomAvatar: AvatarData?, + onSuggestionSelected: (RoomMemberSuggestion) -> Unit, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier.clickable { onSuggestionSelected(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) { + val avatarSize = AvatarSize.TimelineRoom + val avatarData = when (memberSuggestion) { + is RoomMemberSuggestion.Room -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize) + is RoomMemberSuggestion.Member -> AvatarData( + memberSuggestion.roomMember.userId.value, + memberSuggestion.roomMember.displayName, + memberSuggestion.roomMember.avatarUrl, + avatarSize, + ) + } + val title = when (memberSuggestion) { + is RoomMemberSuggestion.Room -> stringResource(R.string.screen_room_mentions_at_room_title) + is RoomMemberSuggestion.Member -> memberSuggestion.roomMember.displayName + } + + val subtitle = when (memberSuggestion) { + is RoomMemberSuggestion.Room -> "@room" + is RoomMemberSuggestion.Member -> memberSuggestion.roomMember.userId.value + } + + Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp, top = 8.dp, bottom = 8.dp) + .align(Alignment.CenterVertically), + ) { + title?.let { + Text( + text = it, + style = ElementTheme.typography.fontBodyLgRegular, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Text( + text = subtitle, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun MentionSuggestionsPickerView_Preview() { + ElementPreview { + val roomMember = RoomMember( + userId = UserId("@alice:server.org"), + displayName = null, + avatarUrl = null, + membership = RoomMembershipState.JOIN, + isNameAmbiguous = false, + powerLevel = 0L, + normalizedPowerLevel = 0L, + isIgnored = false, + ) + MentionSuggestionsPickerView( + roomId = RoomId("!room:matrix.org"), + roomName = "Room", + roomAvatarData = null, + memberSuggestions = persistentListOf( + RoomMemberSuggestion.Room, + RoomMemberSuggestion.Member(roomMember), + RoomMemberSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")), + ), + onSuggestionSelected = {} + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsProcessor.kt new file mode 100644 index 0000000000..1f4e2b5376 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsProcessor.kt @@ -0,0 +1,111 @@ +/* + * 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.mentions + +import io.element.android.features.messages.impl.messagecomposer.RoomMemberSuggestion +import io.element.android.libraries.matrix.api.core.UserId +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 +import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.textcomposer.model.Suggestion +import io.element.android.libraries.textcomposer.model.SuggestionType + +/** + * This class is responsible for processing mention suggestions when `@`, `/` or `#` are type in the composer. + */ +object MentionSuggestionsProcessor { + + // We don't want to retrieve thousands of members + private const val MAX_BATCH_ITEMS = 100 + + /** + * Process the mention suggestions. + * @param suggestion The current suggestion input + * @param roomMembersState The room members state, it contains the current users in the room + * @param currentUserId The current user id + * @param canSendRoomMention Should return true if the current user can send room mentions + * @return The list of mentions to display + */ + suspend fun process( + suggestion: Suggestion?, + roomMembersState: MatrixRoomMembersState, + currentUserId: UserId, + canSendRoomMention: suspend () -> Boolean, + ): List { + val members = roomMembersState.roomMembers() + // Take the first MAX_BATCH_ITEMS only + ?.take(MAX_BATCH_ITEMS) + return when { + members.isNullOrEmpty() || suggestion == null -> { + // Clear suggestions + emptyList() + } + else -> { + when (suggestion.type) { + SuggestionType.Mention -> { + // Replace suggestions + val matchingMembers = getMemberSuggestions( + query = suggestion.text, + roomMembers = roomMembersState.roomMembers(), + currentUserId = currentUserId, + canSendRoomMention = canSendRoomMention() + ) + matchingMembers + } + else -> { + // Clear suggestions + emptyList() + } + } + } + } + } + + private fun getMemberSuggestions( + query: String, + roomMembers: List?, + currentUserId: UserId, + canSendRoomMention: Boolean, + ): List { + return if (roomMembers.isNullOrEmpty()) { + emptyList() + } else { + fun isJoinedMemberAndNotSelf(member: RoomMember): Boolean { + return member.membership == RoomMembershipState.JOIN && currentUserId != member.userId + } + + fun memberMatchesQuery(member: RoomMember, query: String): Boolean { + return member.userId.value.contains(query, ignoreCase = true) + || member.displayName?.contains(query, ignoreCase = true) == true + } + + val matchingMembers = roomMembers + // Search only in joined members, exclude the current user + .filter { member -> + isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query) + } + .map(RoomMemberSuggestion::Member) + + if ("room".contains(query) && canSendRoomMention) { + listOf(RoomMemberSuggestion.Room) + matchingMembers + } else { + matchingMembers + } + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index cd03c52e1e..97c2e7015d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.textcomposer.model.Suggestion @Immutable sealed interface MessageComposerEvents { @@ -39,4 +40,5 @@ sealed interface MessageComposerEvents { data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents data object CancelSendAttachment : MessageComposerEvents data class Error(val error: Throwable) : MessageComposerEvents + data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents } 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 3857600e58..b66decd81e 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 @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -34,6 +35,7 @@ import im.vector.app.features.analytics.plan.Composer import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.media.local.LocalMediaFactory +import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage @@ -43,22 +45,32 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.ProgressCallback 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.user.CurrentSessionIdHolder import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.services.analytics.api.AnalyticsService import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.coroutines.coroutineContext +import kotlin.time.Duration.Companion.seconds import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes @SingleIn(RoomScope::class) @@ -73,17 +85,26 @@ class MessageComposerPresenter @Inject constructor( private val analyticsService: AnalyticsService, private val messageComposerContext: MessageComposerContextImpl, private val richTextEditorStateFactory: RichTextEditorStateFactory, + private val currentSessionIdHolder: CurrentSessionIdHolder, permissionsPresenterFactory: PermissionsPresenter.Factory ) : Presenter { private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA) private var pendingEvent: MessageComposerEvents? = null + private val suggestionSearchTrigger = MutableStateFlow(null) + + @OptIn(FlowPreview::class) @SuppressLint("UnsafeOptInUsageError") @Composable override fun present(): MessageComposerState { val localCoroutineScope = rememberCoroutineScope() + var isMentionsEnabled by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + isMentionsEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.Mentions) + } + val cameraPermissionState = cameraPermissionPresenter.present() val attachmentsState = remember { mutableStateOf(AttachmentsState.None) @@ -151,6 +172,34 @@ class MessageComposerPresenter @Inject constructor( } } + val memberSuggestions = remember { mutableStateListOf() } + LaunchedEffect(isMentionsEnabled) { + if (!isMentionsEnabled) return@LaunchedEffect + val currentUserId = currentSessionIdHolder.current + + suspend fun canSendRoomMention(): Boolean { + val roomIsDm = room.isDirect && room.isOneToOne + val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false) + return !roomIsDm && userCanSendAtRoom + } + + suggestionSearchTrigger + .debounce(0.5.seconds) + .combine(room.membersStateFlow) { suggestion, roomMembersState -> + memberSuggestions.clear() + val result = MentionSuggestionsProcessor.process( + suggestion = suggestion, + roomMembersState = roomMembersState, + currentUserId = currentUserId, + canSendRoomMention = ::canSendRoomMention, + ) + if (result.isNotEmpty()) { + memberSuggestions.addAll(result) + } + } + .collect() + } + fun handleEvents(event: MessageComposerEvents) { when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value @@ -231,6 +280,9 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerEvents.Error -> { analyticsService.trackError(event.error) } + is MessageComposerEvents.SuggestionReceived -> { + suggestionSearchTrigger.value = event.suggestion + } } } @@ -243,6 +295,7 @@ class MessageComposerPresenter @Inject constructor( canShareLocation = canShareLocation.value, canCreatePoll = canCreatePoll.value, attachmentsState = attachmentsState.value, + memberSuggestions = memberSuggestions.toPersistentList(), eventSink = { handleEvents(it) } ) } @@ -355,3 +408,8 @@ class MessageComposerPresenter @Inject constructor( } } } + +sealed interface RoomMemberSuggestion { + data object Room : RoomMemberSuggestion + data class Member(val roomMember: RoomMember) : RoomMemberSuggestion +} 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 3e8f171626..6a9d963d18 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 @@ -33,6 +33,7 @@ data class MessageComposerState( val canShareLocation: Boolean, val canCreatePoll: Boolean, val attachmentsState: AttachmentsState, + val memberSuggestions: ImmutableList, val eventSink: (MessageComposerEvents) -> Unit, ) { val hasFocus: Boolean = richTextEditorState.hasFocus 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 76f40a1969..ac936b2118 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 @@ -19,6 +19,8 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.wysiwyg.compose.RichTextEditorState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf open class MessageComposerStateProvider : PreviewParameterProvider { override val values: Sequence @@ -36,6 +38,7 @@ fun aMessageComposerState( canShareLocation: Boolean = true, canCreatePoll: Boolean = true, attachmentsState: AttachmentsState = AttachmentsState.None, + memberSuggestions: ImmutableList = persistentListOf(), ) = MessageComposerState( richTextEditorState = composerState, isFullScreen = isFullScreen, @@ -45,5 +48,6 @@ fun aMessageComposerState( canShareLocation = canShareLocation, canCreatePoll = canCreatePoll, attachmentsState = attachmentsState, + memberSuggestions = memberSuggestions, eventSink = {}, ) 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 0c71bdbec1..e34a22eedc 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 @@ -28,11 +28,12 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerStateProvider import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.textcomposer.model.Message +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.textcomposer.TextComposer +import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.PressEvent +import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import kotlinx.coroutines.launch @@ -61,6 +62,10 @@ internal fun MessageComposerView( state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = false)) } + fun onSuggestionReceived(suggestion: Suggestion?) { + state.eventSink(MessageComposerEvents.SuggestionReceived(suggestion)) + } + fun onError(error: Throwable) { state.eventSink(MessageComposerEvents.Error(error)) } @@ -106,6 +111,7 @@ internal fun MessageComposerView( onVoicePlayerEvent = onVoicePlayerEvent, onSendVoiceMessage = onSendVoiceMessage, onDeleteVoiceMessage = onDeleteVoiceMessage, + onSuggestionReceived = ::onSuggestionReceived, onError = ::onError, ) } diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 233a9ecbf4..12b3ec04a1 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -4,6 +4,8 @@ "%1$d room change" "%1$d room changes" + "Notify the whole room" + "Everyone" "Camera" "Take photo" "Record video" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 9709dde50e..3dde2a984f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -63,11 +63,13 @@ 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.MessageEventType import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID 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.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -629,6 +631,7 @@ class MessagesPresenterTest { messageComposerContext = MessageComposerContextImpl(), richTextEditorStateFactory = TestRichTextEditorStateFactory(), permissionsPresenterFactory = permissionsPresenterFactory, + currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), ) val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( this, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 2ae34d018f..f05c209085 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState +import io.element.android.features.messages.impl.messagecomposer.RoomMemberSuggestion import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -42,13 +43,23 @@ 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_REPLY +import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_TRANSACTION_ID +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_NAME +import io.element.android.libraries.matrix.test.FakeMatrixClient 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 import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor @@ -60,6 +71,8 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.textcomposer.model.Suggestion +import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.waitForPredicate @@ -67,6 +80,7 @@ import io.mockk.mockk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import okhttp3.internal.immutableListOf import org.junit.Rule import org.junit.Test import java.io.File @@ -706,6 +720,104 @@ class MessageComposerPresenterTest { } } + @Test + fun `present - room member mention suggestions`() = runTest { + val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN) + val invitedUser = aRoomMember(userId = A_USER_ID_3, membership = RoomMembershipState.INVITE) + val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) + val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) + val room = FakeMatrixRoom( + isDirect = false, + isOneToOne = false, + ).apply { + givenRoomMembersState(MatrixRoomMembersState.Ready( + immutableListOf(currentUser, invitedUser, bob, david), + )) + givenCanTriggerRoomNotification(Result.success(true)) + } + val flagsService = FakeFeatureFlagService( + mapOf( + FeatureFlags.Mentions.key to true, + ) + ) + val presenter = createPresenter(this, room, featureFlagService = flagsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + + // A null suggestion (no suggestion was received) returns nothing + initialState.eventSink(MessageComposerEvents.SuggestionReceived(null)) + assertThat(awaitItem().memberSuggestions).isEmpty() + + // An empty suggestion returns the room and joined members that are not the current user + initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, ""))) + assertThat(awaitItem().memberSuggestions) + .containsExactly(RoomMemberSuggestion.Room, RoomMemberSuggestion.Member(bob), RoomMemberSuggestion.Member(david)) + + // A suggestion containing a part of "room" will also return the room mention + initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "roo"))) + assertThat(awaitItem().memberSuggestions).containsExactly(RoomMemberSuggestion.Room) + + // A non-empty suggestion will return those joined members whose user id matches it + initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "bob"))) + assertThat(awaitItem().memberSuggestions).containsExactly(RoomMemberSuggestion.Member(bob)) + + // A non-empty suggestion will return those joined members whose display name matches it + initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, "dave"))) + assertThat(awaitItem().memberSuggestions).containsExactly(RoomMemberSuggestion.Member(david)) + + // If the suggestion isn't a mention, no suggestions are returned + initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Command, ""))) + assertThat(awaitItem().memberSuggestions).isEmpty() + + // If user has no permission to send `@room` mentions, `RoomMemberSuggestion.Room` is not returned + room.givenCanTriggerRoomNotification(Result.success(false)) + initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, ""))) + assertThat(awaitItem().memberSuggestions) + .containsExactly(RoomMemberSuggestion.Member(bob), RoomMemberSuggestion.Member(david)) + + // If room is a DM, `RoomMemberSuggestion.Room` is not returned + room.givenCanTriggerRoomNotification(Result.success(true)) + room.isDirect + } + } + + @Test + fun `present - room member mention suggestions in a DM`() = runTest { + val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN) + val invitedUser = aRoomMember(userId = A_USER_ID_3, membership = RoomMembershipState.INVITE) + val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) + val david = aRoomMember(userId = A_USER_ID_4, displayName = "Dave", membership = RoomMembershipState.JOIN) + val room = FakeMatrixRoom( + isDirect = true, + isOneToOne = true, + ).apply { + givenRoomMembersState(MatrixRoomMembersState.Ready( + immutableListOf(currentUser, invitedUser, bob, david), + )) + givenCanTriggerRoomNotification(Result.success(true)) + } + val flagsService = FakeFeatureFlagService( + mapOf( + FeatureFlags.Mentions.key to true, + ) + ) + val presenter = createPresenter(this, room, featureFlagService = flagsService) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + + // An empty suggestion returns the joined members that are not the current user, but not the room + initialState.eventSink(MessageComposerEvents.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, ""))) + assertThat(awaitItem().memberSuggestions) + .containsExactly(RoomMemberSuggestion.Member(bob), RoomMemberSuggestion.Member(david)) + } + } + private suspend fun ReceiveTurbine.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState { state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode) skipItems(skipCount) @@ -733,6 +845,7 @@ class MessageComposerPresenterTest { analyticsService, MessageComposerContextImpl(), TestRichTextEditorStateFactory(), + currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter), ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt index 6d3487b3a2..a9f7de4efe 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetScaffold.kt @@ -19,18 +19,19 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.contentColorFor -import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp +import io.element.android.libraries.designsystem.theme.components.bottomsheet.BottomSheetScaffoldState +import io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomBottomSheetScaffold +import io.element.android.libraries.designsystem.theme.components.bottomsheet.rememberBottomSheetScaffoldState @Composable @ExperimentalMaterial3Api @@ -52,7 +53,7 @@ fun BottomSheetScaffold( contentColor: Color = contentColorFor(containerColor), content: @Composable (PaddingValues) -> Unit ) { - androidx.compose.material3.BottomSheetScaffold( + CustomBottomSheetScaffold( sheetContent = sheetContent, modifier = modifier, scaffoldState = scaffoldState, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt new file mode 100644 index 0000000000..22b55c19af --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomBottomSheetScaffold.kt @@ -0,0 +1,519 @@ +/* + * 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. + */ +@file:OptIn(ExperimentalFoundationApi::class) + +package io.element.android.libraries.designsystem.theme.components.bottomsheet + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SheetValue.Expanded +import androidx.compose.material3.SheetValue.PartiallyExpanded +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.collapse +import androidx.compose.ui.semantics.dismiss +import androidx.compose.ui.semantics.expand +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.Velocity +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.roundToInt + +// These are needed until https://issuetracker.google.com/issues/306464779 is fixed + +@Composable +@ExperimentalMaterial3Api +fun CustomBottomSheetScaffold( + sheetContent: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), + sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight, + sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, + sheetShape: Shape = BottomSheetDefaults.ExpandedShape, + sheetContainerColor: Color = BottomSheetDefaults.ContainerColor, + sheetContentColor: Color = contentColorFor(sheetContainerColor), + sheetTonalElevation: Dp = BottomSheetDefaults.Elevation, + sheetShadowElevation: Dp = BottomSheetDefaults.Elevation, + sheetDragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, + sheetSwipeEnabled: Boolean = true, + topBar: @Composable (() -> Unit)? = null, + snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, + containerColor: Color = MaterialTheme.colorScheme.surface, + contentColor: Color = contentColorFor(containerColor), + content: @Composable (PaddingValues) -> Unit +) { + val peekHeightPx = with(LocalDensity.current) { + sheetPeekHeight.roundToPx() + } + CustomBottomSheetScaffoldLayout( + modifier = modifier, + topBar = topBar, + body = content, + snackbarHost = { + snackbarHost(scaffoldState.snackbarHostState) + }, + sheetPeekHeight = sheetPeekHeight, + sheetOffset = { scaffoldState.bottomSheetState.requireOffset() }, + sheetState = scaffoldState.bottomSheetState, + containerColor = containerColor, + contentColor = contentColor, + bottomSheet = { layoutHeight -> + CustomStandardBottomSheet( + state = scaffoldState.bottomSheetState, + peekHeight = sheetPeekHeight, + sheetMaxWidth = sheetMaxWidth, + sheetSwipeEnabled = sheetSwipeEnabled, + calculateAnchors = { sheetSize -> + val sheetHeight = sheetSize.height + io.element.android.libraries.designsystem.theme.components.bottomsheet.DraggableAnchors { + if (!scaffoldState.bottomSheetState.skipPartiallyExpanded) { + PartiallyExpanded at (layoutHeight - peekHeightPx).toFloat() + } + if (sheetHeight != peekHeightPx) { + Expanded at maxOf(layoutHeight - sheetHeight, 0).toFloat() + } + if (!scaffoldState.bottomSheetState.skipHiddenState) { + SheetValue.Hidden at layoutHeight.toFloat() + } + } + }, + shape = sheetShape, + containerColor = sheetContainerColor, + contentColor = sheetContentColor, + tonalElevation = sheetTonalElevation, + shadowElevation = sheetShadowElevation, + dragHandle = sheetDragHandle, + content = sheetContent + ) + } + ) +} + +@SuppressWarnings("ModifierWithoutDefault") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CustomBottomSheetScaffoldLayout( + modifier: Modifier, + topBar: @Composable (() -> Unit)?, + body: @Composable (innerPadding: PaddingValues) -> Unit, + bottomSheet: @Composable (layoutHeight: Int) -> Unit, + snackbarHost: @Composable () -> Unit, + sheetPeekHeight: Dp, + sheetOffset: () -> Float, + sheetState: CustomSheetState, + containerColor: Color, + contentColor: Color, +) { + // b/291735717 Remove this once deprecated methods without density are removed + val density = LocalDensity.current + SideEffect { + sheetState.density = density + } + SubcomposeLayout { constraints -> + val layoutWidth = constraints.maxWidth + val layoutHeight = constraints.maxHeight + val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) + + val sheetPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Sheet) { + bottomSheet(layoutHeight) + }[0].measure(looseConstraints) + + val topBarPlaceable = topBar?.let { + subcompose(BottomSheetScaffoldLayoutSlot.TopBar) { topBar() }[0] + .measure(looseConstraints) + } + val topBarHeight = topBarPlaceable?.height ?: 0 + + val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight) + val bodyPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Body) { + Surface( + modifier = modifier, + color = containerColor, + contentColor = contentColor, + ) { body(PaddingValues(bottom = sheetPeekHeight)) } + }[0].measure(bodyConstraints) + + val snackbarPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Snackbar, snackbarHost)[0] + .measure(looseConstraints) + + layout(layoutWidth, layoutHeight) { + val sheetOffsetY = sheetOffset().roundToInt() + val sheetOffsetX = Integer.max(0, (layoutWidth - sheetPlaceable.width) / 2) + + val snackbarOffsetX = (layoutWidth - snackbarPlaceable.width) / 2 + val snackbarOffsetY = when (sheetState.currentValue) { + SheetValue.PartiallyExpanded -> sheetOffsetY - snackbarPlaceable.height + SheetValue.Expanded, SheetValue.Hidden -> layoutHeight - snackbarPlaceable.height + } + + // Placement order is important for elevation + bodyPlaceable.placeRelative(0, topBarHeight) + topBarPlaceable?.placeRelative(0, 0) + sheetPlaceable.placeRelative(sheetOffsetX, sheetOffsetY) + snackbarPlaceable.placeRelative(snackbarOffsetX, snackbarOffsetY) + } + } +} + +private enum class BottomSheetScaffoldLayoutSlot { TopBar, Body, Sheet, Snackbar } + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +private fun CustomStandardBottomSheet( + state: CustomSheetState, + @Suppress("PrimitiveInLambda") + calculateAnchors: (sheetSize: IntSize) -> DraggableAnchors, + peekHeight: Dp, + sheetMaxWidth: Dp, + sheetSwipeEnabled: Boolean, + shape: Shape, + containerColor: Color, + contentColor: Color, + tonalElevation: Dp, + shadowElevation: Dp, + dragHandle: @Composable (() -> Unit)?, + content: @Composable ColumnScope.() -> Unit +) { + val scope = rememberCoroutineScope() + + val orientation = Orientation.Vertical + + Surface( + modifier = Modifier + .widthIn(max = sheetMaxWidth) + .fillMaxWidth() + .requiredHeightIn(min = peekHeight) + .apply { + if (sheetSwipeEnabled) { + nestedScroll( + remember(state.anchoredDraggableState) { + ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( + sheetState = state, + orientation = orientation, + onFling = { scope.launch { state.settle(it) } } + ) + } + ) + } + } + .anchoredDraggable( + state = state.anchoredDraggableState, + orientation = orientation, + enabled = sheetSwipeEnabled + ) + .onSizeChanged { layoutSize -> + val newAnchors = calculateAnchors(layoutSize) + val newTarget = when (state.anchoredDraggableState.targetValue) { + SheetValue.Hidden, SheetValue.PartiallyExpanded -> SheetValue.PartiallyExpanded + SheetValue.Expanded -> { + if (newAnchors.hasAnchorFor(SheetValue.Expanded)) SheetValue.Expanded else SheetValue.PartiallyExpanded + } + } + state.anchoredDraggableState.updateAnchors(newAnchors, newTarget) + }, + shape = shape, + color = containerColor, + contentColor = contentColor, + tonalElevation = tonalElevation, + shadowElevation = shadowElevation, + ) { + Column(Modifier.fillMaxWidth()) { + if (dragHandle != null) { + val partialExpandActionLabel = + "Partial Expand" + val dismissActionLabel = "Dismiss" + val expandActionLabel = "Expand" + Box( + Modifier + .align(Alignment.CenterHorizontally) + .semantics(mergeDescendants = true) { + with(state) { + // Provides semantics to interact with the bottomsheet if there is more + // than one anchor to swipe to and swiping is enabled. + if (anchoredDraggableState.anchors.size > 1 && sheetSwipeEnabled) { + if (currentValue == SheetValue.PartiallyExpanded) { + expand(expandActionLabel) { + scope.launch { expand() }; true + } + } else { + collapse(partialExpandActionLabel) { + scope.launch { partialExpand() }; true + } + } + if (!state.skipHiddenState) { + dismiss(dismissActionLabel) { + scope.launch { hide() } + true + } + } + } + } + }, + ) { + dragHandle() + } + } + content() + } + } +} + +/** + * [DraggableAnchorsConfig] stores a mutable configuration anchors, comprised of values of [T] and + * corresponding [Float] positions. This [DraggableAnchorsConfig] is used to construct an immutable + * [DraggableAnchors] instance later on. + */ +@ExperimentalFoundationApi +class DraggableAnchorsConfig { + + internal val anchors = mutableMapOf() + + /** + * Set the anchor position for [this] anchor. + * + * @param position The anchor position. + */ + @Suppress("BuilderSetStyle") + infix fun T.at(position: Float) { + anchors[this] = position + } +} + +/** + * Create a new [DraggableAnchors] instance using a builder function. + * + * @param T The type of the anchor values. + * @param builder A function with a [DraggableAnchorsConfig] that offers APIs to configure anchors + * @return A new [DraggableAnchors] instance with the anchor positions set by the `builder` + * function. + */ +@OptIn(ExperimentalFoundationApi::class) +@ExperimentalMaterial3Api +@SuppressWarnings("FunctionName") +internal fun DraggableAnchors( + builder: DraggableAnchorsConfig.() -> Unit +): DraggableAnchors = MapDraggableAnchors(DraggableAnchorsConfig().apply(builder).anchors) + +private class MapDraggableAnchors(private val anchors: Map) : DraggableAnchors { + + override fun positionOf(value: T): Float = anchors[value] ?: Float.NaN + override fun hasAnchorFor(value: T) = anchors.containsKey(value) + + override fun closestAnchor(position: Float): T? = anchors.minByOrNull { + abs(position - it.value) + }?.key + + override fun closestAnchor( + position: Float, + searchUpwards: Boolean + ): T? { + return anchors.minByOrNull { (_, anchor) -> + val delta = if (searchUpwards) anchor - position else position - anchor + if (delta < 0) Float.POSITIVE_INFINITY else delta + }?.key + } + + override fun minAnchor() = anchors.values.minOrNull() ?: Float.NaN + + override fun maxAnchor() = anchors.values.maxOrNull() ?: Float.NaN + + override val size: Int + get() = anchors.size + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MapDraggableAnchors<*>) return false + + return anchors == other.anchors + } + + override fun hashCode() = 31 * anchors.hashCode() + + override fun toString() = "MapDraggableAnchors($anchors)" +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@SuppressWarnings("FunctionName") +internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( + sheetState: CustomSheetState, + orientation: Orientation, + onFling: (velocity: Float) -> Unit +): NestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val delta = available.toFloat() + return if (delta < 0 && source == NestedScrollSource.Drag) { + sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset() + } else { + Offset.Zero + } + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + return if (source == NestedScrollSource.Drag) { + sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset() + } else { + Offset.Zero + } + } + + override suspend fun onPreFling(available: Velocity): Velocity { + val toFling = available.toFloat() + val currentOffset = sheetState.requireOffset() + val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor() + return if (toFling < 0 && currentOffset > minAnchor) { + onFling(toFling) + // since we go to the anchor with tween settling, consume all for the best UX + available + } else { + Velocity.Zero + } + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + onFling(available.toFloat()) + return available + } + + private fun Float.toOffset(): Offset = Offset( + x = if (orientation == Orientation.Horizontal) this else 0f, + y = if (orientation == Orientation.Vertical) this else 0f + ) + + @JvmName("velocityToFloat") + private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y + + @JvmName("offsetToFloat") + private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y +} + +/** + * State of the [BottomSheetScaffold] composable. + * + * @param bottomSheetState the state of the persistent bottom sheet + * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold + */ +@ExperimentalMaterial3Api +@Stable +@SuppressWarnings("UseDataClass") +class BottomSheetScaffoldState( + val bottomSheetState: CustomSheetState, + val snackbarHostState: SnackbarHostState +) + +/** + * Create and [remember] a [BottomSheetScaffoldState]. + * + * @param bottomSheetState the state of the standard bottom sheet. See + * [rememberStandardBottomSheetState] + * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold + */ +@Composable +@ExperimentalMaterial3Api +fun rememberBottomSheetScaffoldState( + bottomSheetState: CustomSheetState = rememberStandardBottomSheetState(), + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } +): BottomSheetScaffoldState { + return remember(bottomSheetState, snackbarHostState) { + BottomSheetScaffoldState( + bottomSheetState = bottomSheetState, + snackbarHostState = snackbarHostState + ) + } +} + +/** + * Create and [remember] a [SheetState] for [BottomSheetScaffold]. + * + * @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or + * [Expanded] if [skipHiddenState] is true + * @param confirmValueChange optional callback invoked to confirm or veto a pending state change + * @param [skipHiddenState] whether Hidden state is skipped for [BottomSheetScaffold] + */ +@Composable +@ExperimentalMaterial3Api +fun rememberStandardBottomSheetState( + initialValue: SheetValue = PartiallyExpanded, + confirmValueChange: (SheetValue) -> Boolean = { true }, + skipHiddenState: Boolean = true, +) = rememberSheetState(false, confirmValueChange, initialValue, skipHiddenState) + +@Composable +@ExperimentalMaterial3Api +internal fun rememberSheetState( + skipPartiallyExpanded: Boolean = false, + confirmValueChange: (SheetValue) -> Boolean = { true }, + initialValue: SheetValue = SheetValue.Hidden, + skipHiddenState: Boolean = false, +): CustomSheetState { + + val density = LocalDensity.current + return rememberSaveable( + skipPartiallyExpanded, confirmValueChange, + saver = CustomSheetState.Saver( + skipPartiallyExpanded = skipPartiallyExpanded, + confirmValueChange = confirmValueChange, + density = density + ) + ) { + CustomSheetState( + skipPartiallyExpanded, + density, + initialValue, + confirmValueChange, + skipHiddenState + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt new file mode 100644 index 0000000000..9a2ff713db --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/bottomsheet/CustomSheetState.kt @@ -0,0 +1,309 @@ +/* + * 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.theme.components.bottomsheet + +import androidx.compose.animation.core.SpringSpec +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.animateTo +import androidx.compose.foundation.gestures.snapTo +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetValue +import androidx.compose.material3.SheetValue.Expanded +import androidx.compose.material3.SheetValue.Hidden +import androidx.compose.material3.SheetValue.PartiallyExpanded +import androidx.compose.runtime.Stable +import androidx.compose.runtime.saveable.Saver +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CancellationException + +@OptIn(ExperimentalFoundationApi::class) +@Stable +@ExperimentalMaterial3Api +class CustomSheetState @Deprecated( + message = "This constructor is deprecated. " + + "Please use the constructor that provides a [Density]", + replaceWith = ReplaceWith( + "SheetState(" + + "skipPartiallyExpanded, LocalDensity.current, initialValue, " + + "confirmValueChange, skipHiddenState)" + ) +) constructor( + internal val skipPartiallyExpanded: Boolean, + initialValue: SheetValue = Hidden, + confirmValueChange: (SheetValue) -> Boolean = { true }, + internal val skipHiddenState: Boolean = false, +) { + + /** + * State of a sheet composable, such as [ModalBottomSheet] + * + * Contains states relating to its swipe position as well as animations between state values. + * + * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large + * enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move + * to the [Hidden] state if available when hiding the sheet, either programmatically or by user + * interaction. + * @param density The density that this state can use to convert values to and from dp. + * @param initialValue The initial value of the state. + * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change. + * @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always + * expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either + * programmatically or by user interaction. + */ + @ExperimentalMaterial3Api + @Suppress("Deprecation") + constructor( + skipPartiallyExpanded: Boolean, + density: Density, + initialValue: SheetValue = Hidden, + confirmValueChange: (SheetValue) -> Boolean = { true }, + skipHiddenState: Boolean = false, + ) : this(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState) { + this.density = density + } + init { + if (skipPartiallyExpanded) { + require(initialValue != PartiallyExpanded) { + "The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " + + "is set to true." + } + } + if (skipHiddenState) { + require(initialValue != Hidden) { + "The initial value must not be set to Hidden if skipHiddenState is set to true." + } + } + } + + /** + * The current value of the state. + * + * If no swipe or animation is in progress, this corresponds to the state the bottom sheet is + * currently in. If a swipe or an animation is in progress, this corresponds the state the sheet + * was in before the swipe or animation started. + */ + + val currentValue: SheetValue get() = anchoredDraggableState.currentValue + + /** + * The target value of the bottom sheet state. + * + * If a swipe is in progress, this is the value that the sheet would animate to if the + * swipe finishes. If an animation is running, this is the target value of that animation. + * Finally, if no swipe or animation is in progress, this is the same as the [currentValue]. + */ + val targetValue: SheetValue get() = anchoredDraggableState.targetValue + + /** + * Whether the modal bottom sheet is visible. + */ + val isVisible: Boolean + get() = anchoredDraggableState.currentValue != Hidden + + /** + * Require the current offset (in pixels) of the bottom sheet. + * + * The offset will be initialized during the first measurement phase of the provided sheet + * content. + * + * These are the phases: + * Composition { -> Effects } -> Layout { Measurement -> Placement } -> Drawing + * + * During the first composition, an [IllegalStateException] is thrown. In subsequent + * compositions, the offset will be derived from the anchors of the previous pass. Always prefer + * accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next + * frame, after layout. + * + * @throws IllegalStateException If the offset has not been initialized yet + */ + fun requireOffset(): Float = anchoredDraggableState.requireOffset() + + fun getOffset(): Float? = anchoredDraggableState.offset.takeIf { !it.isNaN() } + + /** + * Whether the sheet has an expanded state defined. + */ + + val hasExpandedState: Boolean + get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded) + + /** + * Whether the modal bottom sheet has a partially expanded state defined. + */ + val hasPartiallyExpandedState: Boolean + get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded) + + /** + * Fully expand the bottom sheet with animation and suspend until it is fully expanded or + * animation has been cancelled. + * * + * @throws [CancellationException] if the animation is interrupted + */ + suspend fun expand() { + anchoredDraggableState.animateTo(Expanded) + } + + /** + * Animate the bottom sheet and suspend until it is partially expanded or animation has been + * cancelled. + * @throws [CancellationException] if the animation is interrupted + * @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true + */ + suspend fun partialExpand() { + check(!skipPartiallyExpanded) { + "Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" + + " skipPartiallyExpanded to false to use this function." + } + animateTo(PartiallyExpanded) + } + + /** + * Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined + * else [Expanded]. + * @throws [CancellationException] if the animation is interrupted + */ + suspend fun show() { + val targetValue = when { + hasPartiallyExpandedState -> PartiallyExpanded + else -> Expanded + } + animateTo(targetValue) + } + + /** + * Hide the bottom sheet with animation and suspend until it is fully hidden or animation has + * been cancelled. + * @throws [CancellationException] if the animation is interrupted + */ + suspend fun hide() { + check(!skipHiddenState) { + "Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" + + " to false to use this function." + } + animateTo(Hidden) + } + + /** + * Animate to a [targetValue]. + * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the + * [targetValue] without updating the offset. + * + * @throws CancellationException if the interaction interrupted by another interaction like a + * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. + * + * @param targetValue The target value of the animation + * @param velocity The velocity of the animation + */ + @OptIn(ExperimentalFoundationApi::class) + internal suspend fun animateTo( + targetValue: SheetValue, + velocity: Float = anchoredDraggableState.lastVelocity + ) { + anchoredDraggableState.animateTo(targetValue, velocity) + } + + /** + * Snap to a [targetValue] without any animation. + * + * @throws CancellationException if the interaction interrupted by another interaction like a + * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call. + * + * @param targetValue The target value of the animation + */ + @OptIn(ExperimentalFoundationApi::class) + internal suspend fun snapTo(targetValue: SheetValue) { + anchoredDraggableState.snapTo(targetValue) + } + + /** + * Find the closest anchor taking into account the velocity and settle at it with an animation. + */ + @OptIn(ExperimentalFoundationApi::class) + internal suspend fun settle(velocity: Float) { + anchoredDraggableState.settle(velocity) + } + + @OptIn(ExperimentalFoundationApi::class) + internal var anchoredDraggableState = androidx.compose.foundation.gestures.AnchoredDraggableState( + initialValue = initialValue, + animationSpec = AnchoredDraggableDefaults.AnimationSpec, + confirmValueChange = confirmValueChange, + positionalThreshold = { with(requireDensity()) { 56.dp.toPx() } }, + velocityThreshold = { with(requireDensity()) { 125.dp.toPx() } } + ) + + @OptIn(ExperimentalFoundationApi::class) + internal val offset: Float? get() = anchoredDraggableState.offset + + internal var density: Density? = null + private fun requireDensity() = requireNotNull(density) { + "SheetState did not have a density attached. Are you using SheetState with " + + "BottomSheetScaffold or ModalBottomSheet component?" + } + + companion object { + /** + * The default [Saver] implementation for [SheetState]. + */ + @SuppressWarnings("FunctionName") + fun Saver( + skipPartiallyExpanded: Boolean, + confirmValueChange: (SheetValue) -> Boolean, + density: Density + ) = Saver( + save = { it.currentValue }, + restore = { savedValue -> + CustomSheetState(skipPartiallyExpanded, density, savedValue, confirmValueChange) + } + ) + + /** + * The default [Saver] implementation for [SheetState]. + */ + @Deprecated( + message = "This function is deprecated. Please use the overload where Density is" + + " provided.", + replaceWith = ReplaceWith( + "Saver(skipPartiallyExpanded, confirmValueChange, LocalDensity.current)" + ) + ) + @Suppress("Deprecation", "FunctionName") + fun Saver( + skipPartiallyExpanded: Boolean, + confirmValueChange: (SheetValue) -> Boolean + ) = Saver( + save = { it.currentValue }, + restore = { savedValue -> + CustomSheetState(skipPartiallyExpanded, savedValue, confirmValueChange) + } + ) + } +} + +@Stable +@ExperimentalMaterial3Api +internal object AnchoredDraggableDefaults { + /** + * The default animation used by [AnchoredDraggableState]. + */ + @get:ExperimentalMaterial3Api + @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") + @ExperimentalMaterial3Api + val AnimationSpec = SpringSpec() +} 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 e078b634d1..819679082b 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 @@ -60,5 +60,11 @@ enum class FeatureFlags( title = "Element call in rooms", description = "Allow user to start or join a call in a room", defaultValue = false, + ), + Mentions( + key = "feature.mentions", + title = "Mentions", + description = "Type `@` to get mention suggestions and insert them", + defaultValue = 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 87a797d13a..7ff59c7512 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 @@ -38,6 +38,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.VoiceMessages -> false FeatureFlags.PinUnlock -> false FeatureFlags.InRoomCalls -> false + FeatureFlags.Mentions -> false } } else { false 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 1cce3f03b7..23db1ef00f 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 @@ -130,6 +130,8 @@ interface MatrixRoom : Closeable { suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result + suspend fun canUserTriggerRoomNotification(userId: UserId): Result + suspend fun updateAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt new file mode 100644 index 0000000000..64e618ccdf --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/CurrentSessionIdHolder.kt @@ -0,0 +1,30 @@ +/* + * 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.api.user + +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.SessionId +import javax.inject.Inject + +@SingleIn(SessionScope::class) +class CurrentSessionIdHolder @Inject constructor(matrixClient: MatrixClient) { + val current = matrixClient.sessionId + + fun isCurrentSession(sessionId: SessionId?): Boolean = current == sessionId +} 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 d45508e424..44f095a38c 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 @@ -349,6 +349,12 @@ class RustMatrixRoom( } } + override suspend fun canUserTriggerRoomNotification(userId: UserId): Result { + return runCatching { + innerRoom.canUserTriggerRoomNotification(userId.value) + } + } + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result { return sendAttachment(listOf(file, thumbnailFile)) { innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher()) 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 844a049a40..a2adf0b0bb 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 @@ -71,6 +71,7 @@ class FakeMatrixRoom( override val alternativeAliases: List = emptyList(), override val isPublic: Boolean = true, override val isDirect: Boolean = false, + override val isOneToOne: Boolean = false, override val joinedMemberCount: Long = 123L, override val activeMemberCount: Long = 234L, val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), @@ -106,6 +107,7 @@ class FakeMatrixRoom( private var progressCallbackValues = emptyList>() private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io") private var getWidgetDriverResult: Result = Result.success(FakeWidgetDriver()) + private var canUserTriggerRoomNotificationResult: Result = Result.success(true) val editMessageCalls = mutableListOf>() var sendMediaCount = 0 @@ -270,6 +272,10 @@ class FakeMatrixRoom( return canSendEventResults[type] ?: Result.failure(IllegalStateException("No fake answer")) } + override suspend fun canUserTriggerRoomNotification(userId: UserId): Result { + return canUserTriggerRoomNotificationResult + } + override suspend fun sendImage( file: File, thumbnailFile: File, @@ -434,6 +440,10 @@ class FakeMatrixRoom( canSendEventResults[type] = result } + fun givenCanTriggerRoomNotification(result: Result) { + canUserTriggerRoomNotificationResult = result + } + fun givenIgnoreResult(result: Result) { ignoreResult = result } 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 73c1764de2..adb81019d6 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 @@ -37,6 +37,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -75,6 +76,7 @@ import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.PressEvent import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent +import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -83,6 +85,7 @@ import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlin.time.Duration.Companion.seconds +import uniffi.wysiwyg_composer.MenuAction @Composable fun TextComposer( @@ -104,6 +107,7 @@ fun TextComposer( onSendVoiceMessage: () -> Unit = {}, onDeleteVoiceMessage: () -> Unit = {}, onError: (Throwable) -> Unit = {}, + onSuggestionReceived: (Suggestion?) -> Unit = {}, ) { val onSendClicked = { val html = if (enableTextFormatting) state.messageHtml else null @@ -122,27 +126,31 @@ fun TextComposer( .fillMaxSize() .height(IntrinsicSize.Min) - val composerOptionsButton = @Composable { - ComposerOptionsButton( - modifier = Modifier - .size(48.dp), - onClick = onAddAttachment - ) + val composerOptionsButton: @Composable () -> Unit = remember { + @Composable { + ComposerOptionsButton( + modifier = Modifier + .size(48.dp), + onClick = onAddAttachment + ) + } } - val textInput = @Composable { - TextInput( - state = state, - subcomposing = subcomposing, - placeholder = if (composerMode.inThread) { - stringResource(id = CommonStrings.action_reply_in_thread) - } else { - stringResource(id = R.string.rich_text_editor_composer_placeholder) - }, - composerMode = composerMode, - onResetComposerMode = onResetComposerMode, - onError = onError, - ) + val textInput: @Composable () -> Unit = remember(state, subcomposing, composerMode, onResetComposerMode, onError) { + @Composable { + TextInput( + state = state, + subcomposing = subcomposing, + placeholder = if (composerMode.inThread) { + stringResource(id = CommonStrings.action_reply_in_thread) + } else { + stringResource(id = R.string.rich_text_editor_composer_placeholder) + }, + composerMode = composerMode, + onResetComposerMode = onResetComposerMode, + onError = onError, + ) + } } val canSendMessage by remember { derivedStateOf { state.messageHtml.isNotEmpty() } } @@ -249,6 +257,16 @@ fun TextComposer( SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } } + + val menuAction = state.menuAction + LaunchedEffect(menuAction) { + if (menuAction is MenuAction.Suggestion) { + val suggestion = Suggestion(menuAction.suggestionPattern) + onSuggestionReceived(suggestion) + } else { + onSuggestionReceived(null) + } + } } @Composable diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Suggestion.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Suggestion.kt new file mode 100644 index 0000000000..e3174f74d0 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Suggestion.kt @@ -0,0 +1,50 @@ +/* + * 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.textcomposer.model + +import uniffi.wysiwyg_composer.PatternKey +import uniffi.wysiwyg_composer.SuggestionPattern + +data class Suggestion( + val start: Int, + val end: Int, + val type: SuggestionType, + val text: String, +) { + constructor(suggestion: SuggestionPattern): this( + suggestion.start.toInt(), + suggestion.end.toInt(), + SuggestionType.fromPatternKey(suggestion.key), + suggestion.text, + ) +} + +enum class SuggestionType { + Mention, + Command, + Room; + + companion object { + fun fromPatternKey(key: PatternKey): SuggestionType { + return when (key) { + PatternKey.AT -> Mention + PatternKey.SLASH -> Command + PatternKey.HASH -> Room + } + } + } +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt index f4bd7175fd..6ed58b61e8 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt @@ -35,6 +35,7 @@ class KonsistArchitectureTest { .withNameEndingWith("State") .withoutName( "CameraPositionState", + "CustomSheetState", ) .constructors .parameters diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.mentions_null_MentionSuggestionsPickerView_-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.mentions_null_MentionSuggestionsPickerView_-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6a628f5fea --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.mentions_null_MentionSuggestionsPickerView_-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9a2c753096d4b2a363e5e0811e3017e3a20fd1c23d2ad26133434c651c4c4cb +size 18686 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.mentions_null_MentionSuggestionsPickerView_-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.mentions_null_MentionSuggestionsPickerView_-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fc371b38fd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.mentions_null_MentionSuggestionsPickerView_-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e52de245ef2c4a423849ab0ff592109e51c2c25e0cbc1335812a37986c27b06 +size 18411 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-4_4_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-3_3_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-4_4_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-4_5_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-3_4_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-4_5_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-5_5_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-5_5_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-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.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-5_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-5_5_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-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.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-5_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-6_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-6_6_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-D-7_7_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-6_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-6_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-6_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-6_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-6_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-6_7_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-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.messages.impl.report_null_ReportMessageView-N-6_7_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-D-25_25_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-D-26_26_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.customreaction_null_EmojiItem-D-25_25_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-D-26_26_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-N-25_26_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-N-26_27_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.customreaction_null_EmojiItem-N-25_26_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiItem-N-26_27_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-26_26_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-27_27_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.customreaction_null_EmojiPicker-D-26_26_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-D-27_27_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-26_27_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-27_28_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.customreaction_null_EmojiPicker-N-26_27_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPicker-N-27_28_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-27_27_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_0,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.event_null_TimelineItemAudioView-D-27_27_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-27_27_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_1,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.event_null_TimelineItemAudioView-D-27_27_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-27_27_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_2,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.event_null_TimelineItemAudioView-D-27_27_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-D-28_28_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-27_28_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_0,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.event_null_TimelineItemAudioView-N-27_28_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-27_28_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_1,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.event_null_TimelineItemAudioView-N-27_28_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-27_28_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_2,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.event_null_TimelineItemAudioView-N-27_28_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemAudioView-N-28_29_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-28_28_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-29_29_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.event_null_TimelineItemEncryptedView-D-28_28_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-D-29_29_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-28_29_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-29_30_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.event_null_TimelineItemEncryptedView-N-28_29_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemEncryptedView-N-29_30_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-29_29_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_0,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.event_null_TimelineItemFileView-D-29_29_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-29_29_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_1,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.event_null_TimelineItemFileView-D-29_29_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-29_29_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_2,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.event_null_TimelineItemFileView-D-29_29_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-D-30_30_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-29_30_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_0,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.event_null_TimelineItemFileView-N-29_30_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-29_30_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_1,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.event_null_TimelineItemFileView-N-29_30_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-29_30_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_2,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.event_null_TimelineItemFileView-N-29_30_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemFileView-N-30_31_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-30_30_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_0,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.event_null_TimelineItemImageView-D-30_30_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-30_30_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_1,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.event_null_TimelineItemImageView-D-30_30_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-30_30_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_2,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.event_null_TimelineItemImageView-D-30_30_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-D-31_31_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-30_31_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_0,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.event_null_TimelineItemImageView-N-30_31_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-30_31_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_1,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.event_null_TimelineItemImageView-N-30_31_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-30_31_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_2,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.event_null_TimelineItemImageView-N-30_31_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemImageView-N-31_32_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-31_31_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-32_32_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.event_null_TimelineItemInformativeView-D-31_31_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-D-32_32_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-31_32_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-32_33_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.event_null_TimelineItemInformativeView-N-31_32_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemInformativeView-N-32_33_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-32_32_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-33_33_null_0,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.event_null_TimelineItemLocationView-D-32_32_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-33_33_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-32_32_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-33_33_null_1,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.event_null_TimelineItemLocationView-D-32_32_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-D-33_33_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-32_33_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-33_34_null_0,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.event_null_TimelineItemLocationView-N-32_33_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-33_34_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-32_33_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-33_34_null_1,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.event_null_TimelineItemLocationView-N-32_33_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemLocationView-N-33_34_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-34_34_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-35_35_null_0,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.event_null_TimelineItemPollCreatorView-D-34_34_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-35_35_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-34_34_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-35_35_null_1,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.event_null_TimelineItemPollCreatorView-D-34_34_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-D-35_35_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-34_35_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-35_36_null_0,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.event_null_TimelineItemPollCreatorView-N-34_35_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-35_36_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-34_35_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-35_36_null_1,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.event_null_TimelineItemPollCreatorView-N-34_35_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollCreatorView-N-35_36_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-33_33_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-34_34_null_0,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.event_null_TimelineItemPollView-D-33_33_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-34_34_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-33_33_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-34_34_null_1,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.event_null_TimelineItemPollView-D-33_33_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-34_34_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-33_34_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-34_35_null_0,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.event_null_TimelineItemPollView-N-33_34_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-34_35_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-33_34_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-34_35_null_1,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.event_null_TimelineItemPollView-N-33_34_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-34_35_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-35_35_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-36_36_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.event_null_TimelineItemRedactedView-D-35_35_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-D-36_36_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-35_36_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-36_37_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.event_null_TimelineItemRedactedView-N-35_36_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemRedactedView-N-36_37_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-36_36_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-37_37_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.event_null_TimelineItemStateView-D-36_36_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-D-37_37_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-36_37_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-37_38_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.event_null_TimelineItemStateView-N-36_37_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemStateView-N-37_38_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_0,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.event_null_TimelineItemTextView-D-37_37_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_1,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.event_null_TimelineItemTextView-D-37_37_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_2,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.event_null_TimelineItemTextView-D-37_37_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_3,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.event_null_TimelineItemTextView-D-37_37_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_4,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.event_null_TimelineItemTextView-D-37_37_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-37_37_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_5,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.event_null_TimelineItemTextView-D-37_37_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-D-38_38_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_0,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.event_null_TimelineItemTextView-N-37_38_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_1,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.event_null_TimelineItemTextView-N-37_38_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_2,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.event_null_TimelineItemTextView-N-37_38_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_3,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.event_null_TimelineItemTextView-N-37_38_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_4,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.event_null_TimelineItemTextView-N-37_38_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-37_38_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_5,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.event_null_TimelineItemTextView-N-37_38_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemTextView-N-38_39_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-38_38_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-39_39_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.event_null_TimelineItemUnknownView-D-38_38_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-D-39_39_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-38_39_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-39_40_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.event_null_TimelineItemUnknownView-N-38_39_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemUnknownView-N-39_40_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-39_39_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_0,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.event_null_TimelineItemVideoView-D-39_39_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-39_39_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_1,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.event_null_TimelineItemVideoView-D-39_39_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-39_39_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_2,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.event_null_TimelineItemVideoView-D-39_39_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-D-40_40_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-39_40_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_0,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.event_null_TimelineItemVideoView-N-39_40_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-39_40_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_1,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.event_null_TimelineItemVideoView-N-39_40_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-39_40_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_2,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.event_null_TimelineItemVideoView-N-39_40_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVideoView-N-40_41_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_0,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.event_null_TimelineItemVoiceView-D-40_40_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_1,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.event_null_TimelineItemVoiceView-D-40_40_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_10,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.event_null_TimelineItemVoiceView-D-40_40_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_11,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.event_null_TimelineItemVoiceView-D-40_40_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_12,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.event_null_TimelineItemVoiceView-D-40_40_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_13,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.event_null_TimelineItemVoiceView-D-40_40_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_14,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.event_null_TimelineItemVoiceView-D-40_40_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_2,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.event_null_TimelineItemVoiceView-D-40_40_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_3,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.event_null_TimelineItemVoiceView-D-40_40_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_4,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.event_null_TimelineItemVoiceView-D-40_40_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_5,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.event_null_TimelineItemVoiceView-D-40_40_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_6,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.event_null_TimelineItemVoiceView-D-40_40_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_7,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.event_null_TimelineItemVoiceView-D-40_40_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_8,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.event_null_TimelineItemVoiceView-D-40_40_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_9,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.event_null_TimelineItemVoiceView-D-40_40_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,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.event_null_TimelineItemVoiceView-N-40_41_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,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.event_null_TimelineItemVoiceView-N-40_41_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,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.event_null_TimelineItemVoiceView-N-40_41_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,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.event_null_TimelineItemVoiceView-N-40_41_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,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.event_null_TimelineItemVoiceView-N-40_41_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,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.event_null_TimelineItemVoiceView-N-40_41_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,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.event_null_TimelineItemVoiceView-N-40_41_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,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.event_null_TimelineItemVoiceView-N-40_41_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,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.event_null_TimelineItemVoiceView-N-40_41_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,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.event_null_TimelineItemVoiceView-N-40_41_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,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.event_null_TimelineItemVoiceView-N-40_41_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,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.event_null_TimelineItemVoiceView-N-40_41_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,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.event_null_TimelineItemVoiceView-N-40_41_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,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.event_null_TimelineItemVoiceView-N-40_41_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,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.event_null_TimelineItemVoiceView-N-40_41_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-42_42_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.event_null_TimelineItemVoiceViewUnified-D-41_41_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-42_42_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_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.event_null_TimelineItemVoiceViewUnified-N-41_42_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-42_42_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-43_43_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.group_null_GroupHeaderView-D-42_42_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-D-43_43_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-42_43_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-43_44_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.group_null_GroupHeaderView-N-42_43_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_null_GroupHeaderView-N-43_44_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_0,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.html_null_HtmlDocument-D-43_43_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_1,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.html_null_HtmlDocument-D-43_43_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_10,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.html_null_HtmlDocument-D-43_43_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_11,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.html_null_HtmlDocument-D-43_43_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_12,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.html_null_HtmlDocument-D-43_43_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_13,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.html_null_HtmlDocument-D-43_43_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_14,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.html_null_HtmlDocument-D-43_43_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_15,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.html_null_HtmlDocument-D-43_43_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_16,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.html_null_HtmlDocument-D-43_43_null_16,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_16,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_17,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.html_null_HtmlDocument-D-43_43_null_17,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_17,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_18,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.html_null_HtmlDocument-D-43_43_null_18,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_18,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_19,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.html_null_HtmlDocument-D-43_43_null_19,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_19,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_2,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.html_null_HtmlDocument-D-43_43_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_20,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.html_null_HtmlDocument-D-43_43_null_20,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_20,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_21,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.html_null_HtmlDocument-D-43_43_null_21,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_21,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_3,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.html_null_HtmlDocument-D-43_43_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_4,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.html_null_HtmlDocument-D-43_43_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_5,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.html_null_HtmlDocument-D-43_43_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_6,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.html_null_HtmlDocument-D-43_43_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_7,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.html_null_HtmlDocument-D-43_43_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_8,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.html_null_HtmlDocument-D-43_43_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-43_43_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_9,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.html_null_HtmlDocument-D-43_43_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-D-44_44_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_0,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.html_null_HtmlDocument-N-43_44_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_1,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.html_null_HtmlDocument-N-43_44_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_10,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.html_null_HtmlDocument-N-43_44_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_11,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.html_null_HtmlDocument-N-43_44_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_12,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.html_null_HtmlDocument-N-43_44_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_13,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.html_null_HtmlDocument-N-43_44_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_14,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.html_null_HtmlDocument-N-43_44_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_15,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.html_null_HtmlDocument-N-43_44_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_16,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.html_null_HtmlDocument-N-43_44_null_16,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_16,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_17,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.html_null_HtmlDocument-N-43_44_null_17,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_17,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_18,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.html_null_HtmlDocument-N-43_44_null_18,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_18,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_19,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.html_null_HtmlDocument-N-43_44_null_19,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_19,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_2,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.html_null_HtmlDocument-N-43_44_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_20,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.html_null_HtmlDocument-N-43_44_null_20,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_20,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_21,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.html_null_HtmlDocument-N-43_44_null_21,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_21,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_3,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.html_null_HtmlDocument-N-43_44_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_4,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.html_null_HtmlDocument-N-43_44_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_5,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.html_null_HtmlDocument-N-43_44_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_6,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.html_null_HtmlDocument-N-43_44_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_7,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.html_null_HtmlDocument-N-43_44_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_8,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.html_null_HtmlDocument-N-43_44_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-43_44_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_9,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.html_null_HtmlDocument-N-43_44_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_null_HtmlDocument-N-44_45_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-44_44_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-45_45_null_0,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.reactionsummary_null_SheetContent-D-44_44_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-D-45_45_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-44_45_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-45_46_null_0,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.reactionsummary_null_SheetContent-N-44_45_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.reactionsummary_null_SheetContent-N-45_46_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-45_45_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-46_46_null_0,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.retrysendmenu_null_RetrySendMessageMenu-D-45_45_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-46_46_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-45_45_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-46_46_null_1,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.retrysendmenu_null_RetrySendMessageMenu-D-45_45_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-D-46_46_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-45_46_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-46_47_null_0,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.retrysendmenu_null_RetrySendMessageMenu-N-45_46_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-46_47_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-45_46_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-46_47_null_1,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.retrysendmenu_null_RetrySendMessageMenu-N-45_46_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_null_RetrySendMessageMenu-N-46_47_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_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_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-47_47_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_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_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_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-47_48_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_null_TimelineItemDaySeparatorView-D-47_47_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-48_48_null_0,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_null_TimelineItemDaySeparatorView-D-47_47_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-48_48_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-47_47_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-D-48_48_null_1,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_null_TimelineItemDaySeparatorView-D-47_47_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_null_TimelineItemDaySeparatorView-D-48_48_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-47_48_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-48_49_null_0,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_null_TimelineItemDaySeparatorView-N-47_48_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-48_49_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-47_48_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineItemDaySeparatorView-N-48_49_null_1,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_null_TimelineItemDaySeparatorView-N-47_48_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_null_TimelineItemDaySeparatorView-N-48_49_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-48_48_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-49_49_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_null_TimelineLoadingMoreIndicator-D-48_48_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-D-49_49_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_null_TimelineLoadingMoreIndicator-N-48_49_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-49_50_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_null_TimelineLoadingMoreIndicator-N-48_49_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineLoadingMoreIndicator-N-49_50_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_0,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_null_MessageEventBubble-D-8_8_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_1,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_null_MessageEventBubble-D-8_8_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_10,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_null_MessageEventBubble-D-8_8_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_11,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_null_MessageEventBubble-D-8_8_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_12,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_null_MessageEventBubble-D-8_8_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_13,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_null_MessageEventBubble-D-8_8_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_14,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_null_MessageEventBubble-D-8_8_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_15,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_null_MessageEventBubble-D-8_8_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_2,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_null_MessageEventBubble-D-8_8_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_3,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_null_MessageEventBubble-D-8_8_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_4,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_null_MessageEventBubble-D-8_8_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_5,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_null_MessageEventBubble-D-8_8_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_6,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_null_MessageEventBubble-D-8_8_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_7,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_null_MessageEventBubble-D-8_8_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_8,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_null_MessageEventBubble-D-8_8_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-8_8_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_9,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_null_MessageEventBubble-D-8_8_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-D-9_9_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_13,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_13,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_14,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_14,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_15,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_15,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-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.messages.impl.timeline.components_null_MessageEventBubble-N-8_9_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageEventBubble-N-9_10_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-9_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-10_10_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_null_MessageStateEventContainer-D-9_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-D-10_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-9_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-10_11_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_null_MessageStateEventContainer-N-9_10_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessageStateEventContainer-N-10_11_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-12_12_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_null_MessagesAddReactionButton-D-11_11_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-D-12_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-12_13_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_null_MessagesAddReactionButton-N-11_12_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesAddReactionButton-N-12_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_0,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_null_MessagesReactionButton-D-10_10_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_1,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_null_MessagesReactionButton-D-10_10_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_2,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_null_MessagesReactionButton-D-10_10_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-10_10_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_3,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_null_MessagesReactionButton-D-10_10_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-D-11_11_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_0,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_null_MessagesReactionButton-N-10_11_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-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.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_2,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_null_MessagesReactionButton-N-10_11_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-10_11_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_3,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_null_MessagesReactionButton-N-10_11_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionButton-N-11_12_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-12_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-13_13_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_null_MessagesReactionExtraButtons-D-12_12_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-D-13_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-12_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-13_14_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_null_MessagesReactionExtraButtons-N-12_13_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_MessagesReactionExtraButtons-N-13_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-13_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-14_14_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_null_ReplySwipeIndicator-D-13_13_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-D-14_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-13_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-14_15_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_null_ReplySwipeIndicator-N-13_14_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_ReplySwipeIndicator-N-14_15_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_0,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_null_TimelineEventTimestampView-D-14_14_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_1,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_null_TimelineEventTimestampView-D-14_14_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_2,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_null_TimelineEventTimestampView-D-14_14_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-14_14_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_3,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_null_TimelineEventTimestampView-D-14_14_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-D-15_15_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_0,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_null_TimelineEventTimestampView-N-14_15_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_1,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_null_TimelineEventTimestampView-N-14_15_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_2,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_null_TimelineEventTimestampView-N-14_15_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-14_15_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_3,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_null_TimelineEventTimestampView-N-14_15_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineEventTimestampView-N-15_16_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-16_16_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_null_TimelineItemEventRow-D-15_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-D-16_16_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-16_17_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_null_TimelineItemEventRow-N-15_16_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRow-N-16_17_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_0,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_null_TimelineItemEventRowTimestamp-D-17_17_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_1,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_null_TimelineItemEventRowTimestamp-D-17_17_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_2,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_null_TimelineItemEventRowTimestamp-D-17_17_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-17_17_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_3,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_null_TimelineItemEventRowTimestamp-D-17_17_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-D-18_18_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_0,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_null_TimelineItemEventRowTimestamp-N-17_18_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_1,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_null_TimelineItemEventRowTimestamp-N-17_18_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_2,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_null_TimelineItemEventRowTimestamp-N-17_18_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-17_18_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_3,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_null_TimelineItemEventRowTimestamp-N-17_18_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowTimestamp-N-18_19_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-18_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-19_19_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_null_TimelineItemEventRowWithManyReactions-D-18_18_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-D-19_19_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-18_19_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-19_20_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_null_TimelineItemEventRowWithManyReactions-N-18_19_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithManyReactions-N-19_20_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-17_17_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_null_TimelineItemEventRowWithReply-D-16_16_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-D-17_17_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-17_18_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_null_TimelineItemEventRowWithReply-N-16_17_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventRowWithReply-N-17_18_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-19_19_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-20_20_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_null_TimelineItemReactionsLayout-D-19_19_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-D-20_20_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-19_20_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-20_21_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_null_TimelineItemReactionsLayout-N-19_20_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsLayout-N-20_21_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-20_20_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-21_21_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_null_TimelineItemReactionsView-D-20_20_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-D-21_21_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-20_21_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-21_22_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_null_TimelineItemReactionsView-N-20_21_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsView-N-21_22_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-21_21_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-22_22_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_null_TimelineItemReactionsViewFew-D-21_21_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-D-22_22_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-21_22_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-22_23_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_null_TimelineItemReactionsViewFew-N-21_22_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewFew-N-22_23_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-22_22_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-23_23_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_null_TimelineItemReactionsViewIncoming-D-22_22_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-D-23_23_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-22_23_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-23_24_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_null_TimelineItemReactionsViewIncoming-N-22_23_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewIncoming-N-23_24_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-23_23_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-24_24_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_null_TimelineItemReactionsViewOutgoing-D-23_23_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-D-24_24_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-23_24_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-24_25_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_null_TimelineItemReactionsViewOutgoing-N-23_24_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemReactionsViewOutgoing-N-24_25_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-24_24_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-25_25_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_null_TimelineItemStateEventRow-D-24_24_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-D-25_25_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-24_25_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-25_26_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_null_TimelineItemStateEventRow-N-24_25_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemStateEventRow-N-25_26_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-49_49_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-50_50_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.debug_null_EventDebugInfoView-D-49_49_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-D-50_50_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-49_50_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-50_51_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.debug_null_EventDebugInfoView-N-49_50_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_null_EventDebugInfoView-N-50_51_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_12,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-7_7_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-7_8_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-7_8_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_10,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_10,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_10,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_11,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_11,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_11,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_12,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_12,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_12,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-7_8_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-7_8_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-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.messages.impl.timeline_null_TimelineView-N-7_8_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_5,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_6,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_7,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_9,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-7_8_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_9,NEXUS_5,1.0,en].png diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 5144dc191d..547dfc2d0b 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -112,6 +112,7 @@ "name": ":features:messages:impl", "includeRegex": [ "screen_room_.*", + "screen\\.room\\..*", "screen_dm_details_.*", "room_timeline_state_changes" ], From b418e5dbdc5568f4dd8ad23075e7f6be3fe2b6b1 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 27 Oct 2023 10:42:35 +0000 Subject: [PATCH 214/281] Update screenshots --- ...i_null_LogoutPreferenceView-D-0_0_null,NEXUS_5,1.0,en].png | 3 --- ...i_null_LogoutPreferenceView-N-0_1_null,NEXUS_5,1.0,en].png | 3 --- ...gout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png | 3 +++ ...gout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png | 3 +++ ...ll_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...l_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...l_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ll_ConfirmRecoveryKeyBanner-D-3_3_null,NEXUS_5,1.0,en].png | 3 +++ ...ll_ConfirmRecoveryKeyBanner-N-3_4_null,NEXUS_5,1.0,en].png | 3 +++ ...null_DefaultRoomListTopBar-D-5_5_null,NEXUS_5,1.0,en].png} | 0 ...null_DefaultRoomListTopBar-N-5_6_null,NEXUS_5,1.0,en].png} | 0 ...RoomListTopBarWithIndicator-D-6_6_null,NEXUS_5,1.0,en].png | 3 +++ ...RoomListTopBarWithIndicator-N-6_7_null,NEXUS_5,1.0,en].png | 3 +++ ..._RequestVerificationHeader-D-4_4_null,NEXUS_5,1.0,en].png} | 0 ..._RequestVerificationHeader-N-4_5_null,NEXUS_5,1.0,en].png} | 0 ..._RoomSummaryPlaceholderRow-D-7_7_null,NEXUS_5,1.0,en].png} | 0 ..._RoomSummaryPlaceholderRow-N-7_8_null,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-D-8_8_null_0,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-D-8_8_null_1,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-D-8_8_null_2,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-D-8_8_null_3,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-D-8_8_null_4,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-D-8_8_null_5,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-D-8_8_null_6,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-D-8_8_null_7,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-N-8_9_null_0,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-N-8_9_null_1,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-N-8_9_null_2,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-N-8_9_null_3,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-N-8_9_null_4,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-N-8_9_null_5,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-N-8_9_null_6,NEXUS_5,1.0,en].png} | 0 ...ents_null_RoomSummaryRow-N-8_9_null_7,NEXUS_5,1.0,en].png} | 0 ...oomListSearchResultContent-D-9_9_null,NEXUS_5,1.0,en].png} | 0 ...omListSearchResultContent-N-9_10_null,NEXUS_5,1.0,en].png} | 0 ...st.impl_null_RoomListView-D-2_2_null_9,NEXUS_5,1.0,en].png | 3 +++ ...st.impl_null_RoomListView-N-2_3_null_9,NEXUS_5,1.0,en].png | 3 +++ ...l_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...l_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...l_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...l_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png | 3 +++ ...l_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...l_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...l_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...l_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png | 3 +++ ...ll_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ll_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ll_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ll_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ll_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ll_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png | 3 +++ ...ackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-D-3_3_null_0,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-D-3_3_null_1,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-D-3_3_null_2,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-D-3_3_null_3,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-D-3_3_null_4,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-D-3_3_null_5,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-D-3_3_null_6,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-N-3_4_null_0,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-N-3_4_null_1,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-N-3_4_null_2,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-N-3_4_null_3,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-N-3_4_null_4,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-N-3_4_null_5,NEXUS_5,1.0,en].png | 3 +++ ...null_SecureBackupRootView-N-3_4_null_6,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_0,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ews_null_RecoveryKeyView-D-6_6_null_10,NEXUS_5,1.0,en].png | 3 +++ ...ews_null_RecoveryKeyView-D-6_6_null_11,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_2,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_3,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_4,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_5,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_6,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_7,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_8,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-D-6_6_null_9,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_0,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ews_null_RecoveryKeyView-N-6_7_null_10,NEXUS_5,1.0,en].png | 3 +++ ...ews_null_RecoveryKeyView-N-6_7_null_11,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_2,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_3,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_4,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_5,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_6,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_7,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_8,NEXUS_5,1.0,en].png | 3 +++ ...iews_null_RecoveryKeyView-N-6_7_null_9,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png | 3 +++ ...ull_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png | 3 +++ ...cureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png | 3 +++ ..._Preferences_PreferenceTextDark_0_null,NEXUS_5,1.0,en].png | 3 +++ ...Preferences_PreferenceTextLight_0_null,NEXUS_5,1.0,en].png | 3 +++ ..._PreferenceTextWithEndBadgeDark_0_null,NEXUS_5,1.0,en].png | 3 +++ ...PreferenceTextWithEndBadgeLight_0_null,NEXUS_5,1.0,en].png | 3 +++ ...null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png | 3 --- 135 files changed, 320 insertions(+), 17 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.api_null_LogoutPreferenceView-D-0_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.api_null_LogoutPreferenceView-N-0_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_ConfirmRecoveryKeyBanner-D-3_3_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_ConfirmRecoveryKeyBanner-N-3_4_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-4_4_null,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-5_5_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-4_5_null,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-5_6_null,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarWithIndicator-D-6_6_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarWithIndicator-N-6_7_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-4_4_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-4_5_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-D-5_5_null,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-D-7_7_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-N-5_6_null,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-N-7_8_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_6,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_7,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_7,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-D-7_7_null,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-D-9_9_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-N-7_8_null,NEXUS_5,1.0,en].png => ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-N-9_10_null,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_9,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_9,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_11,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_9,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_11,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_9,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextDark_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextLight_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextWithEndBadgeDark_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextWithEndBadgeLight_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.api_null_LogoutPreferenceView-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.api_null_LogoutPreferenceView-D-0_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 4e323f3bb1..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.api_null_LogoutPreferenceView-D-0_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a648ab1e1a85e4442378e16dc6ccf81fdfd7e8d04903519d239b6858f7b8488 -size 7081 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.api_null_LogoutPreferenceView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.api_null_LogoutPreferenceView-N-0_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index b20bb92b2f..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.api_null_LogoutPreferenceView-N-0_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3628567719c53c3590f4932e56f45b8a10fc76f4ce6dc77ac620c507f9190729 -size 6872 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..716325cb97 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8c36602ad1aa6a62cae8f69b4e0a185715a4105a958a17451e3f5573618a499 +size 28590 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ba29e536fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30777b75b1464bbaf0ab63c73189ae8d6b380d677ae87c22ad59a9052dbda564 +size 32530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..48c0b62d21 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23d11018a66dcfca97713d7dfd5916a1c00f01881c21aad6014101a17b5b8912 +size 30167 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6e7f76aa42 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b3b64d82224c1e28cfad0d5ac81796a2461311d63d23e84f9bf19b290b08bcd +size 37255 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4bfb9042c4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aeee9a02b5bfa15ff7dc2e45f374237b2fa198df9e3a0a92598a950af7431a5b +size 34809 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..292fdcf260 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5437f1dd640de8500ce78fe810a92c5174879918b592e746f09a7244b4b98d74 +size 31993 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7018ed9a82 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3c05dd55dfd945a7c7528492d47698586089b618ae6ab811ae188a808a0a4b8 +size 33355 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..71b951b61d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39091e9262d0d57d51468ae809598d44829a2a33759e62f18154444c0c614f50 +size 26893 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bab7e54b27 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:424a67c305485dc3d151d7771df9b9e724aa076e3758dc9cb341b86184163a39 +size 30551 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3dcfbdb383 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a1e679206fef656da4fbcba44220cc169d45e01faf74c6bdb174a807ccf89a2 +size 28529 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..935078e5a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f1a452799ce1f0055c17ec86a5dabe7d3f5f5688b15bc9e9f5cdde7c39774b7 +size 35541 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5bfc16a2b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4fa2444ab2941d044c71011bdb3ecaed621f125dd8752526b8485aaf80e1b63 +size 30747 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9c73ebc664 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63b528173b275d15fb8c0ec5d173b63636f24a3897227f4fb37e06a817a7521e +size 28881 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5ef2025508 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f200497bd6e06ed304a919c32b6622c711e8d570f7376be3518ab7fe0eb1a2f4 +size 29427 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png index 79ac6aa584..38c7bbb003 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eec57772a4c2390238b7363d91d02cc99124ca442a66a9049155f86313506b43 -size 45307 +oid sha256:7c79d4722b2cba273d2ce6c71a533c338810adf2919ac163de27f4c49a132981 +size 43962 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png index a142e1a34b..9115ed0420 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06eaf42a48e6ece74f4192de66731e4dfc23617034ddf1cbedb18eb392e3e10a -size 44638 +oid sha256:48dbab6995ee03ccb67678229e804d96b471dbc7db48d0e7dce0b61f69d53dfb +size 43285 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png index 5b33dfe72e..94bd1a0a72 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f860899fb9c33ab3af97dee35bcd39d9b4c128fed4c0256b2c354dc9193d1a0b -size 48393 +oid sha256:e6f75db2d6b6fe5e655fef4c74781e9c9bc00408c569a679837f4fb5529b4508 +size 46866 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png index 80cb7c6241..18e7ca8615 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3eb0577c673b6da2bbf35d4eeb485c2af08cdea3e084dbf123befb2d092bbcd -size 48300 +oid sha256:8b34af47ea56faa01a23c9bd12620153d1d79a522bbcab42ad4051bdf19270ae +size 46758 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_ConfirmRecoveryKeyBanner-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_ConfirmRecoveryKeyBanner-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..298e5bc5c7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_ConfirmRecoveryKeyBanner-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86af49a59603dfe9feddf14c84a738135f5a23e73e4c8f6d616b45f2643af3ee +size 29415 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_ConfirmRecoveryKeyBanner-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_ConfirmRecoveryKeyBanner-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..db7d9fd507 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_ConfirmRecoveryKeyBanner-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c938be2898cd6eb51ab4bf2cbdeabaab3b89fda02b2328f8c149b6e6aa7f3242 +size 28840 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-5_5_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_null_DefaultRoomListTopBar-D-4_4_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-D-5_5_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-5_6_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_null_DefaultRoomListTopBar-N-4_5_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBar-N-5_6_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarWithIndicator-D-6_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarWithIndicator-D-6_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1ce758b42c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarWithIndicator-D-6_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:773de163162ca8d1d38d65aea3dbcc145a29547643f756e6a6719f9910357036 +size 37286 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarWithIndicator-N-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarWithIndicator-N-6_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7798eb9edb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_DefaultRoomListTopBarWithIndicator-N-6_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c72953d38cc652d0c6c5737f6e115650855cf295505fe38adab5932a542b480 +size 42650 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-4_4_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_null_RequestVerificationHeader-D-3_3_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-D-4_4_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-4_5_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_null_RequestVerificationHeader-N-3_4_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RequestVerificationHeader-N-4_5_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-D-5_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-D-7_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_null_RoomSummaryPlaceholderRow-D-5_5_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-D-7_7_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-N-5_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-N-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_null_RoomSummaryPlaceholderRow-N-5_6_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryPlaceholderRow-N-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_null_RoomSummaryRow-D-6_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_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_null_RoomSummaryRow-D-6_6_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_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_null_RoomSummaryRow-D-6_6_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_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_null_RoomSummaryRow-D-6_6_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_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_null_RoomSummaryRow-D-6_6_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_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_null_RoomSummaryRow-D-6_6_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_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_null_RoomSummaryRow-D-6_6_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_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_null_RoomSummaryRow-D-6_6_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-6_6_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_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_null_RoomSummaryRow-D-6_6_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-D-8_8_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-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.roomlist.impl.components_null_RoomSummaryRow-N-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_null_RoomSummaryRow-N-8_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-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.roomlist.impl.components_null_RoomSummaryRow-N-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_null_RoomSummaryRow-N-8_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-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.roomlist.impl.components_null_RoomSummaryRow-N-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_null_RoomSummaryRow-N-8_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-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.roomlist.impl.components_null_RoomSummaryRow-N-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_null_RoomSummaryRow-N-8_9_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-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.roomlist.impl.components_null_RoomSummaryRow-N-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_null_RoomSummaryRow-N-8_9_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_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_null_RoomSummaryRow-N-6_7_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_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_null_RoomSummaryRow-N-6_7_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-6_7_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_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_null_RoomSummaryRow-N-6_7_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_null_RoomSummaryRow-N-8_9_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-D-7_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-D-9_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.search_null_RoomListSearchResultContent-D-7_7_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-D-9_9_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-N-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-N-9_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.search_null_RoomListSearchResultContent-N-7_8_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.search_null_RoomListSearchResultContent-N-9_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..912c8eb2e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-D-2_2_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a10e3677e9550a0579bcbda29c31303229638a838ced006c678697507340c514 +size 89949 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0ebd806966 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_null_RoomListView-N-2_3_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d7e2aed5d5ec7f81334ba81b621ea489f4a59d40024f8560ed4084725c348fc +size 91560 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1090a99a76 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b042f7e1248b739be1b0b96782d3970c0bfafa06c48ca507acbe882df471de3 +size 58735 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d60a41a849 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6df1c5767b684843e39d31e6056133e72eb76d5d229db0d7176ff1be02852880 +size 48100 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..369174bf3a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2c79522043f06f1a2d5810b1e2206d8b6d2ccea18389fb9a11a18887dc2d895 +size 59411 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0941246116 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f5e1a5524453cc1814322315bde1180aa1e26f3f2bf22ec5e797c8767bd8675 +size 30634 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ca51028a14 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49f4d57ff2771fb32f198b506f0a0b7b7fd4f8f0af7f5b78c2369dfaec2e6543 +size 57060 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..39812bd3f0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ab1a52d75b027259c855c63b2b626f286899963b251e5ac9540478b0f7988c3 +size 42813 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2689301c41 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d34567f158c2480ba0c6884663b8c996d4754e069f558103b8de90a25a22a8e0 +size 57550 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c1577d4d23 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36152a1506067dafcc6591d0ef0a29e68b96df40070eb8f039fac484f152f6b7 +size 27091 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..be946fad50 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ffce711923870d9b43a9d4c2871ac1794d3d7e2b34a2063ab6f9e56c4e22dc0 +size 15203 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..95865f5b0d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ef1cae71114c56ad6173cdeef356da56ce2769d5bd4a1332133f048d241a8d1 +size 15777 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fb72a4d9b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82d71934698b4e25afaaf9024c5dbb0571c3f8fd3270b5b07ff8d8dd9045bdd9 +size 21841 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..087096b93e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4036acde06bd1fb9e256a97b53a6711ec45db4b67141b01231b491512608e457 +size 14271 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f8f8f17e86 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d4d58607c126ae4a01a0ba62e09666fac11d3a2b05596742bc4e25cf75696fb +size 14830 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7e318fb1a8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b19e0f154e435885299c579360112ecf04c14f67f141ebe855a7dc32052f962a +size 18636 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c4852d22de --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90d7dab73ed32794eceac418541c2cf851d1e8bae0fdd5c34f8d9f0425890dc1 +size 33998 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..87ad6fd5e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9949ae01bf9aa195e88cb4d06ec0abd61e4bc3dee402f0cdf6617b7559ec318 +size 46006 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..74be4421e3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:386e41bb476411e1836be260ae309361b1493a914508484fda618783dffae00d +size 44521 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..96139ea253 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9893ff64a23c98a13892235f36c52e2ab5ce5a57b3ee7953b884d81ebcfcd89 +size 33201 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9122b9ec31 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:691eb429d1e3a799d99213225a2bd92fa0b7b3ed8b384c8910ba114b5e05b514 +size 32221 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9a0a4ad046 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27e4d8474ca09ee1a76947464c1b8bd3c6aeb2f0b73b67cca5736d62d92e286d +size 42834 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bb8bbedc22 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:119fa492046cbfa51e9eb406fea4c4a9a6fb7dd152e3779153a2df2947800f76 +size 41383 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1879d86aa6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01455558e19098ab432b0b7cb48c1be2e9d2b2d0b1e97035143687d202687b13 +size 29643 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d5ae1fe7c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aef9057653b917425621448d0ef62975b02b6cae0e572758a2749ac9cc71189 +size 39364 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0abdbfd76d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92ce51c885eb9c5efb80b747ef107e185114401d235bfd261ec06a1bb9f53f85 +size 42518 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0a55353c4c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c830fe829915419ea9917b201a80ab13a40c2c37783ac0e8b52bfcc5d0f8ae9 +size 42461 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d5ae1fe7c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aef9057653b917425621448d0ef62975b02b6cae0e572758a2749ac9cc71189 +size 39364 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..471a6b98c6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b159c6b39ad05b3d0dc10ae0da1826bcaae45c94e0150b09c2cf6ddf2353d305 +size 24831 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d5ae1fe7c3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aef9057653b917425621448d0ef62975b02b6cae0e572758a2749ac9cc71189 +size 39364 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f75b484e1f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-D-3_3_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb83490002e584be9f268eadc014345740d0026c929e79f2bb4c407fdf2ba926 +size 29777 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..35f90db1d9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:870c36e4f7a64e258af75e22845d05dd8c8217d6dc65826b6405537bed9bc848 +size 37439 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3a7029466e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19965ae446d383e69f8f05dc3e3cb1d3956a9962c472d9246b765ae467ef8d56 +size 40318 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d8b61cb909 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89e3cf46598c82bc2957ba497a4018654067b157e9da23b23240213c152a5b8e +size 40163 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..35f90db1d9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:870c36e4f7a64e258af75e22845d05dd8c8217d6dc65826b6405537bed9bc848 +size 37439 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a8dd202ac2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:837be69c85fb1a7f08ac0780ca34a56fa5d4d1f2134b4255efc68a78b33cab65 +size 23591 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..35f90db1d9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:870c36e4f7a64e258af75e22845d05dd8c8217d6dc65826b6405537bed9bc848 +size 37439 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ffc413c52d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_null_SecureBackupRootView-N-3_4_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eda4aab1ded791ad60e417818ad49070c1c53b9c1353da6dd843fae0934459a5 +size 28387 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1c2e58a7cf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eca49fe4bdc67ff1ac9b10da19feab91a0efc9705c82b772e15909775765a907 +size 21961 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..adc3eae19e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70fe9112996b3d463283bb51cd30cc5bac92ecc2471a63f8c97808462fd4f299 +size 19819 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0cc3665e1f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5ba80d0d623bc8ddb694d47cfd85e3421ce2baa21e4f19e185c30c86f2fd6a3 +size 24259 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_11,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d4ee38ed5a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_11,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a472e8ecafb6d4d19d1b13d12c234a7886981356fbe8b00c9394d5a130585c07 +size 22320 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ba5734a1d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bdf32ec33c3fdaf7e29e79b95edb7c22fd50362f5ee8d66f653e80b8a3f80f6 +size 23969 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ba5734a1d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bdf32ec33c3fdaf7e29e79b95edb7c22fd50362f5ee8d66f653e80b8a3f80f6 +size 23969 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b9d7e00faa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a01cd38fa99db8e54327c35d1b157a24ec2a100c206ddc45f54f5634d23bf67d +size 22141 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..adc3eae19e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70fe9112996b3d463283bb51cd30cc5bac92ecc2471a63f8c97808462fd4f299 +size 19819 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ba5734a1d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bdf32ec33c3fdaf7e29e79b95edb7c22fd50362f5ee8d66f653e80b8a3f80f6 +size 23969 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ba5734a1d4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bdf32ec33c3fdaf7e29e79b95edb7c22fd50362f5ee8d66f653e80b8a3f80f6 +size 23969 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4743f2e9d8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +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_null_RecoveryKeyView-D-6_6_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cb977e1604 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-D-6_6_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +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_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c319814f17 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:721f24dec2187a504722f9e055c364b1157126d48014e0f88172593ccdbc9e56 +size 20931 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..45d5e54d51 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07478df93245af67fa863480313facd7b4d2923f284446b55f956e45ec5fb2f1 +size 18905 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fb3bfe76ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3c30ea61015a894b1e2f8c3a576fe83862233c5b3675489c786cff589bf3164 +size 22718 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_11,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6140c50709 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_11,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec5a621733bc6923dd3769afc2d3bab644cc51894039e9b0b233716a68a1edbc +size 20673 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f1d810dd81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94e746ed3248ba456891b669fbb76c89a1924c571f7ebfc21fa72547e3119d15 +size 22868 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f1d810dd81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94e746ed3248ba456891b669fbb76c89a1924c571f7ebfc21fa72547e3119d15 +size 22868 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..97c0b0cc9d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97dfb9633ae86b94e95d5db567400e7ac84f25a8b66705dea6d623437434d329 +size 21136 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..45d5e54d51 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07478df93245af67fa863480313facd7b4d2923f284446b55f956e45ec5fb2f1 +size 18905 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f1d810dd81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94e746ed3248ba456891b669fbb76c89a1924c571f7ebfc21fa72547e3119d15 +size 22868 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f1d810dd81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94e746ed3248ba456891b669fbb76c89a1924c571f7ebfc21fa72547e3119d15 +size 22868 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..106212d72b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +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_null_RecoveryKeyView-N-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_null_RecoveryKeyView-N-6_7_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2fc240d96f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup.views_null_RecoveryKeyView-N-6_7_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +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_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1821bef52b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2f9a83985ed5f607f6f6618794857468c4e71451bb58ba160478f8fb39a78a8 +size 51611 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ebae223f43 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cad87dcb5fb62c8d577c6e0ac4a13681a827c9d9bd1e2880d3cc3ad802802376 +size 49737 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..92a65fe89d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac22cb35d4842fdab146bcaa8b89b6b09e53354c79fb6c865ab3b76b08c35a7 +size 53186 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..92a65fe89d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac22cb35d4842fdab146bcaa8b89b6b09e53354c79fb6c865ab3b76b08c35a7 +size 53186 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9619617245 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14fd53641ce5e6d7246bbd575ba4d91d73a1ddda332fa134619afd011e9086bb +size 51267 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1bc25a1b2a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:385b8e5012fad2197297fa1782226a0938a63e53bc4ed1dfa13d82b885a2ca74 +size 49691 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c1cd47b777 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e87bac6fa87595efb59245e2289ec7badeab60b959f7d99898cb404cf3c484f4 +size 47704 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7800db3ce5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f02343264623c0ee8d1eeb3ea48702f45d672bb4f319fd78e6eb488aab10a0 +size 50268 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7800db3ce5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f02343264623c0ee8d1eeb3ea48702f45d672bb4f319fd78e6eb488aab10a0 +size 50268 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1481c6abdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:035ef744899cd61549684482dccb8845018e4f574bdfdcdb57d33fb98a23ef3b +size 45743 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..066d300929 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dba09f9888d8efd1036c66baf702d05bdd3f4a26c39ca14ae5c59ae2571d00b7 +size 50245 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1fc4133905 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad43dd16b6b0232978b83795b739dcb71778a3a0faf7b8a4da599f23ec653237 +size 48125 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..92a65fe89d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac22cb35d4842fdab146bcaa8b89b6b09e53354c79fb6c865ab3b76b08c35a7 +size 53186 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..92a65fe89d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ac22cb35d4842fdab146bcaa8b89b6b09e53354c79fb6c865ab3b76b08c35a7 +size 53186 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9619617245 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14fd53641ce5e6d7246bbd575ba4d91d73a1ddda332fa134619afd011e9086bb +size 51267 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..085a9a11e9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67c874c41e3ce38f3cf2ccc6cf3759d6fec0a675fffa9cea94df341a8792c61e +size 48296 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d8fd472ea7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b32141d05f2e1b89eab149b91e02354b7db464a7c0b42516da74e18af777aa6 +size 46048 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7800db3ce5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f02343264623c0ee8d1eeb3ea48702f45d672bb4f319fd78e6eb488aab10a0 +size 50268 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7800db3ce5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8f02343264623c0ee8d1eeb3ea48702f45d672bb4f319fd78e6eb488aab10a0 +size 50268 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1481c6abdc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:035ef744899cd61549684482dccb8845018e4f574bdfdcdb57d33fb98a23ef3b +size 45743 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextDark_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f7c75aed61 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextDark_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc5c04af9085f12cbf4ffd5beddfe95f683358716bcb5676e235bf61cef0d037 +size 36257 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextLight_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..03840a0a77 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextLight_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30a1b09e10a4f0af425cf946b9cbb1f9c051b2437a1fe46024748b9bb61ea638 +size 37376 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextWithEndBadgeDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextWithEndBadgeDark_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a8f6b06222 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextWithEndBadgeDark_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:719814e14c9adcefdd2bab346c1a510cdafed23f98e5e4dcb8eb7e958355992b +size 38154 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextWithEndBadgeLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextWithEndBadgeLight_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..10ed3ed71f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceTextWithEndBadgeLight_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81ebf584035f87cf30edeb3d8e00c154154fa504c83471f6bf55b2e62946d99a +size 39486 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 97d734e024..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.preferences_null_Preferences_PreferenceText_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:514851f2cdb43698787949873ca19e2da26287e341dbc43192b76593532ec582 -size 43820 From 9510d432894c52d8fbfc03a66b23307accf870e9 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 27 Oct 2023 12:33:35 +0100 Subject: [PATCH 215/281] Add waveform to voice message preview UI (#1661) * Add waveform to preview UI * Update screenshots * Make random waveform function deterministic * Update screenshots --------- Co-authored-by: ElementBot --- .../impl/voicemessages/WaveformUtils.kt | 7 ++- .../composer/VoiceMessageComposerPresenter.kt | 23 ++++++---- .../VoiceMessageComposerPresenterTest.kt | 43 ++++++++++--------- .../components/media/FakeWaveformFactory.kt | 36 ++++++++++++++++ .../components/media/WaveformPlaybackView.kt | 17 +++++++- .../libraries/textcomposer/TextComposer.kt | 40 ++++++++--------- .../components/VoiceMessagePreview.kt | 34 ++++++++++++--- .../textcomposer/model/VoiceMessageState.kt | 5 ++- .../voicerecorder/test/FakeVoiceRecorder.kt | 3 +- ...gePreview-D-15_15_null,NEXUS_5,1.0,en].png | 4 +- ...gePreview-N-15_16_null,NEXUS_5,1.0,en].png | 4 +- ...mposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 +- ...mposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 +- 13 files changed, 157 insertions(+), 67 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt index 354e2eba7d..83926f0f22 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt @@ -20,4 +20,9 @@ package io.element.android.features.messages.impl.voicemessages * Resizes the given [0;1024] int list as per unstable MSC3246 spec * to a [0;1] range float list to be used for waveform rendering. */ -fun List.fromMSC3246range(): List = map { it / 1024f } +internal fun List.fromMSC3246range(): List = map { it / 1024f } + +/** + * Resizes the given [0;1] float list to [0;1024] int list as per unstable MSC3246 spec. + */ +internal fun List.toMSC3246range(): List = map { (it * 1024).toInt() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index 5e0d9bcada..f3f86a7bf9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import io.element.android.features.messages.impl.voicemessages.VoiceMessageException +import io.element.android.features.messages.impl.voicemessages.toMSC3246range import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn @@ -40,6 +41,8 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber @@ -66,6 +69,7 @@ class VoiceMessageComposerPresenter @Inject constructor( var isSending by remember { mutableStateOf(false) } val playerState by player.state.collectAsState(initial = VoiceMessageComposerPlayer.State.NotPlaying) val isPlaying by remember(playerState.isPlaying) { derivedStateOf { playerState.isPlaying } } + val waveform by remember(recorderState) { derivedStateOf { recorderState.finishedWaveform() } } val onLifecycleEvent = { event: Lifecycle.Event -> when (event) { @@ -174,11 +178,11 @@ class VoiceMessageComposerPresenter @Inject constructor( duration = state.elapsedTime, level = state.level ) - is VoiceRecorderState.Finished -> if (isSending) { - VoiceMessageState.Sending - } else { - VoiceMessageState.Preview(isPlaying = isPlaying) - } + is VoiceRecorderState.Finished -> VoiceMessageState.Preview( + isSending = isSending, + isPlaying = isPlaying, + waveform = waveform, + ) else -> VoiceMessageState.Idle }, showPermissionRationaleDialog = permissionState.showDialog, @@ -227,7 +231,8 @@ class VoiceMessageComposerPresenter @Inject constructor( } } -/** - * Resizes the given [0;1] float list to [0;1024] int list as per unstable MSC3246 spec. - */ -private fun List.toMSC3246range(): List = map { (it * 1024).toInt() } +private fun VoiceRecorderState.finishedWaveform(): ImmutableList = + (this as? VoiceRecorderState.Finished) + ?.waveform + .orEmpty() + .toImmutableList() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 276ea7a064..955536d9f9 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -25,11 +25,11 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerEvents +import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState -import io.element.android.features.messages.impl.voicemessages.VoiceMessageException -import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.api.MediaSender @@ -44,6 +44,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -216,7 +217,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending) + assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState(isSending = true)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) @@ -239,7 +240,7 @@ class VoiceMessageComposerPresenterTest { eventSink(VoiceMessageComposerEvents.SendVoiceMessage) eventSink(VoiceMessageComposerEvents.SendVoiceMessage) } - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending) + assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState(isSending = true)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) @@ -266,7 +267,7 @@ class VoiceMessageComposerPresenterTest { } val finalState = awaitItem() - assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Sending) + assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true)) assertThat(matrixRoom.sendMediaCount).isEqualTo(0) assertThat(analyticsService.trackedErrors).hasSize(0) voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) @@ -288,7 +289,7 @@ class VoiceMessageComposerPresenterTest { val previewState = awaitItem() previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage) - assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Sending) + assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState(isSending = true)) ensureAllEventsConsumed() assertThat(previewState.voiceMessageState).isEqualTo(aPreviewState()) @@ -452,18 +453,15 @@ class VoiceMessageComposerPresenterTest { VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_PAUSE) ) - val onPauseState = when (val vmState = mostRecentState.voiceMessageState) { - VoiceMessageState.Idle, - VoiceMessageState.Sending -> { - mostRecentState - } + val onPauseState = when (val state = mostRecentState.voiceMessageState) { + VoiceMessageState.Idle -> mostRecentState is VoiceMessageState.Recording -> { // If recorder was active, it stops awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState()) } } - is VoiceMessageState.Preview -> when(vmState.isPlaying) { + is VoiceMessageState.Preview -> when (state.isPlaying) { // If the preview was playing, it pauses true -> awaitItem().apply { assertThat(voiceMessageState).isEqualTo(aPreviewState()) @@ -476,13 +474,15 @@ class VoiceMessageComposerPresenterTest { VoiceMessageComposerEvents.LifecycleEvent(event = Lifecycle.Event.ON_DESTROY) ) - when (onPauseState.voiceMessageState) { - VoiceMessageState.Idle, - VoiceMessageState.Sending -> + when (val state = onPauseState.voiceMessageState) { + VoiceMessageState.Idle -> ensureAllEventsConsumed() - is VoiceMessageState.Recording, - is VoiceMessageState.Preview -> + is VoiceMessageState.Recording -> assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + is VoiceMessageState.Preview -> when (state.isSending) { + true -> ensureAllEventsConsumed() + false -> assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) + } } } @@ -514,9 +514,12 @@ class VoiceMessageComposerPresenterTest { } private fun aPreviewState( - isPlaying: Boolean = false + isPlaying: Boolean = false, + isSending: Boolean = false, + waveform: List = voiceRecorder.waveform, ) = VoiceMessageState.Preview( - isPlaying = isPlaying + isPlaying = isPlaying, + isSending = isSending, + waveform = waveform.toImmutableList(), ) - } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt new file mode 100644 index 0000000000..19b2d4e8b5 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.components.media + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlin.random.Random + +object FakeWaveformFactory { + /** + * Generate a waveform for testing purposes. + * + * The waveform is a list of floats between 0 and 1. + * + * @param length The length of the waveform. + */ + fun createFakeWaveform(length: Int = 1000): ImmutableList { + val random = Random(seed = 2) + return List(length) { random.nextFloat() } + .toPersistentList() + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index b5d472ef69..273d8bf79b 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -54,6 +54,21 @@ import kotlin.math.roundToInt private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F +/** + * A view that displays a waveform and a cursor to indicate the current playback progress. + * + * @param playbackProgress The current playback progress, between 0 and 1. + * @param showCursor Whether to show the cursor or not. + * @param waveform The waveform to display. Use [FakeWaveformFactory] to generate a fake waveform. + * @param modifier The modifier to be applied to the view. + * @param onSeek Callback when the user seeks the waveform. Called with a value between 0 and 1. + * @param brush The brush to use to draw the waveform. + * @param progressBrush The brush to use to draw the progress. + * @param cursorBrush The brush to use to draw the cursor. + * @param lineWidth The width of the waveform lines. + * @param linePadding The padding between waveform lines. + * @param minimumGraphAmplitude The minimum amplitude to display, regardless of waveform data. + */ @OptIn(ExperimentalComposeUiApi::class) @Composable fun WaveformPlaybackView( @@ -78,7 +93,7 @@ fun WaveformPlaybackView( } } val progressAnimated = animateFloatAsState(targetValue = progress, label = "progressAnimation") - val amplitudeDisplayCount by remember(canvasSize) { + val amplitudeDisplayCount by remember(canvasSize, lineWidth, linePadding) { derivedStateOf { (canvasSize.width.value / (lineWidth.value + linePadding.value)).toInt() } 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 adb81019d6..c053dd4100 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 @@ -48,6 +48,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.media.FakeWaveformFactory.createFakeWaveform import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleUp @@ -122,6 +123,10 @@ fun TextComposer( onVoicePlayerEvent(VoiceMessagePlayerEvent.Pause) } + val onSeekVoiceMessage = { position: Float -> + onVoicePlayerEvent(VoiceMessagePlayerEvent.Seek(position)) + } + val layoutModifier = modifier .fillMaxSize() .height(IntrinsicSize.Min) @@ -188,8 +193,10 @@ fun TextComposer( when (voiceMessageState) { VoiceMessageState.Idle, is VoiceMessageState.Recording -> recordVoiceButton - is VoiceMessageState.Preview -> sendVoiceButton - is VoiceMessageState.Sending -> uploadVoiceProgress + is VoiceMessageState.Preview -> when(voiceMessageState.isSending) { + true -> uploadVoiceProgress + false -> sendVoiceButton + } } else -> sendButton @@ -199,17 +206,12 @@ fun TextComposer( when (voiceMessageState) { is VoiceMessageState.Preview -> VoiceMessagePreview( - isInteractive = true, + isInteractive = !voiceMessageState.isSending, isPlaying = voiceMessageState.isPlaying, + waveform = voiceMessageState.waveform, onPlayClick = onPlayVoiceMessageClicked, - onPauseClick = onPauseVoiceMessageClicked - ) - VoiceMessageState.Sending -> - VoiceMessagePreview( - isInteractive = false, - isPlaying = false, - onPlayClick = onPlayVoiceMessageClicked, - onPauseClick = onPauseVoiceMessageClicked + onPauseClick = onPauseVoiceMessageClicked, + onSeek = onSeekVoiceMessage, ) is VoiceMessageState.Recording -> VoiceMessageRecording(voiceMessageState.level, voiceMessageState.duration) @@ -218,13 +220,9 @@ fun TextComposer( } val voiceDeleteButton = @Composable { - val enabled = when (voiceMessageState) { - is VoiceMessageState.Preview -> true - VoiceMessageState.Sending, - is VoiceMessageState.Recording, - VoiceMessageState.Idle -> false + if(voiceMessageState is VoiceMessageState.Preview) { + VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending, onClick = onDeleteVoiceMessage) } - VoiceMessageDeleteButton(enabled = enabled, onClick = onDeleteVoiceMessage) } if (showTextFormatting) { @@ -285,7 +283,7 @@ private fun StandardLayout( verticalAlignment = Alignment.Bottom, ) { if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) { - if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Sending) { + if (voiceMessageState is VoiceMessageState.Preview) { Box( modifier = Modifier .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) @@ -818,11 +816,11 @@ internal fun TextComposerVoicePreview() = ElementPreview { PreviewColumn(items = persistentListOf({ VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, 0.5f)) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Preview(isPlaying = false)) + VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = false, isPlaying = false, waveform = createFakeWaveform())) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Preview(isPlaying = true)) + VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = false, isPlaying = true, waveform = createFakeWaveform())) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Sending) + VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = true, isPlaying = false, waveform = createFakeWaveform())) })) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 4d8521834c..be06f853f8 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -20,10 +20,13 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -31,6 +34,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.media.FakeWaveformFactory.createFakeWaveform +import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleUp @@ -39,14 +44,18 @@ import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.textcomposer.R import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList @Composable internal fun VoiceMessagePreview( isInteractive: Boolean, isPlaying: Boolean, + waveform: ImmutableList, modifier: Modifier = Modifier, + playbackProgress: Float = 0f, onPlayClick: () -> Unit = {}, - onPauseClick: () -> Unit = {} + onPauseClick: () -> Unit = {}, + onSeek: (Float) -> Unit = {}, ) { Row( modifier = modifier @@ -72,7 +81,22 @@ internal fun VoiceMessagePreview( enabled = isInteractive ) } - // TODO Add recording preview UI + + Spacer(modifier = Modifier.width(8.dp)) + + // TODO Add timer UI + + Spacer(modifier = Modifier.width(12.dp)) + + WaveformPlaybackView( + modifier = Modifier + .weight(1f) + .height(26.dp.applyScaleUp()), + playbackProgress = playbackProgress, + showCursor = isInteractive, + waveform = waveform, + onSeek = onSeek, + ) } } @@ -119,8 +143,8 @@ internal fun VoiceMessagePreviewPreview() = ElementPreview { Column( verticalArrangement = Arrangement.spacedBy(8.dp) ) { - VoiceMessagePreview(isInteractive = true, isPlaying = true) - VoiceMessagePreview(isInteractive = true, isPlaying = false) - VoiceMessagePreview(isInteractive = false, isPlaying = false) + VoiceMessagePreview(isInteractive = true, isPlaying = true, waveform = createFakeWaveform()) + VoiceMessagePreview(isInteractive = true, isPlaying = false, waveform = createFakeWaveform()) + VoiceMessagePreview(isInteractive = false, isPlaying = false, waveform = createFakeWaveform()) } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index 012f655ad2..53d5029e11 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -16,15 +16,18 @@ package io.element.android.libraries.textcomposer.model +import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration sealed class VoiceMessageState { data object Idle: VoiceMessageState() data class Preview( + val isSending: Boolean, val isPlaying: Boolean, + val waveform: ImmutableList, ): VoiceMessageState() - data object Sending: VoiceMessageState() + data class Recording( val duration: Duration, val level: Float, diff --git a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt index 7d3f140529..2d3dcaf376 100644 --- a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt +++ b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt @@ -42,6 +42,7 @@ class FakeVoiceRecorder( private var stoppedCount = 0 private var deletedCount = 0 + var waveform: List = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f) override suspend fun startRecord() { startedCount += 1 val startedAt = timeSource.markNow() @@ -73,7 +74,7 @@ class FakeVoiceRecorder( else -> VoiceRecorderState.Finished( file = curRecording!!, mimeType = "audio/ogg", - waveform = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), + waveform = waveform, ) } ) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png index 7fcecb3e63..37e1350a65 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-15_15_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ae485c12f93649418e661ed6f4a3f1393c3e1c703e8fce84e29ff9c4607ca7b -size 8699 +oid sha256:ff4763716e5283a13f7028532cf22953f08d93512bd19ac98e774fd950173ad5 +size 22534 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png index 013ce61eb0..32be676e9e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-15_16_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2afaaa054ed17809447352d245a287854afee0d05c0efd7b00341daf733dc07b -size 6623 +oid sha256:a88306846e0bf2958e235c5b0944376f4c4983ceb026b4da962deb10b249f315 +size 19326 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index 1fc392e02d..ccc7e6c9e1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8eed5b8511637961df60be133688df984560cb84b333a9377e859446dfac2e04 -size 18036 +oid sha256:8fbbbac71323947df0f09b8614b23206add7dbdd36a629668f3474be8b89b5a8 +size 25549 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index 19603922a0..ba316b75f6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14bcc3395316251b2e25a23f95b53030bf2e2ebe0623ef7e113330a5fd2f853d -size 15852 +oid sha256:81863e643154025c515efb0070ceaad2e04b74517ddbd314d214d7d7dae2286d +size 23193 From 977c7e265a8170e203654c6e336a15ba8f2be897 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 27 Oct 2023 13:06:33 +0100 Subject: [PATCH 216/281] Fix naming and waveform previews --- .../composer/VoiceMessageComposerStateProvider.kt | 2 +- .../android/libraries/textcomposer/TextComposer.kt | 2 +- .../textcomposer/components/LiveWaveformView.kt | 14 ++++++++------ .../components/VoiceMessageRecording.kt | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt index f7e3263287..150381461b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt @@ -37,6 +37,6 @@ internal fun aVoiceMessageComposerState( eventSink = {}, ) -internal var aWaveformLevels = List(100) { it.toFloat() / 200 }.toPersistentList() +internal var aWaveformLevels = List(100) { it.toFloat() / 100 }.toPersistentList() 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 fca12fcf48..d601629baa 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 @@ -817,7 +817,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { enableVoiceMessages = true, ) PreviewColumn(items = persistentListOf({ - VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, List(100) { it.toFloat() / 200 }.toPersistentList())) + VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, List(100) { it.toFloat() / 100 }.toPersistentList())) }, { VoicePreview(voiceMessageState = VoiceMessageState.Preview(isPlaying = false)) }, { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt index 0d083fe3e2..d92851c378 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt @@ -58,27 +58,29 @@ fun LiveWaveformView( ) { var canvasSize by remember { mutableStateOf(DpSize(0.dp, 0.dp)) } + var parentWidth by remember { mutableIntStateOf(0) } + val waveformWidth by remember(levels, lineWidth, linePadding) { derivedStateOf { levels.size * (lineWidth.value + linePadding.value) } } - var width by remember { mutableIntStateOf(0) } + Box(contentAlignment = Alignment.CenterEnd, modifier = modifier .fillMaxWidth() .height(waveFormHeight) - .onSizeChanged { width = it.width } + .onSizeChanged { parentWidth = it.width } ) { Canvas( modifier = Modifier - .width(canvasSize.width) + .width(Dp(waveformWidth)) .graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA) .then(modifier) ) { - canvasSize = DpSize(Dp(min(waveformWidth, width.toFloat())), size.height.toDp()) - val countThatFitsWidth = (width.toFloat() / (lineWidth.toPx() + linePadding.toPx())).toInt() + canvasSize = DpSize(Dp(min(waveformWidth, parentWidth.toFloat())), size.height.toDp()) + val countThatFitsWidth = (parentWidth.toFloat() / (lineWidth.toPx() + linePadding.toPx())).toInt() drawWaveform( waveformData = levels.takeLast(countThatFitsWidth).toPersistentList(), canvasSize = canvasSize, @@ -96,7 +98,7 @@ internal fun LiveWaveformViewPreview() = ElementPreview { Column { LiveWaveformView( - levels = List(100) { it.toFloat() / 200 }.toPersistentList(), + levels = List(100) { it.toFloat() / 100 }.toPersistentList(), modifier = Modifier.height(34.dp), ) LiveWaveformView( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 038864d4da..6930441e4b 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -92,5 +92,5 @@ private fun RedRecordingDot( @PreviewsDayNight @Composable internal fun VoiceMessageRecordingPreview() = ElementPreview { - VoiceMessageRecording(List(100) { it.toFloat() / 200 }.toPersistentList(), 0.seconds) + VoiceMessageRecording(List(100) { it.toFloat() / 100 }.toPersistentList(), 0.seconds) } From a6e4644cfa511af4c76ee38a859c0b455d20ef3f Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 14:18:28 +0200 Subject: [PATCH 217/281] Add missing screenshots in TimelineView (#1660) Add audio, voice and poll screenshots to TimelineView which were missing. --- .../messages/impl/timeline/TimelineView.kt | 29 ++++++++++------ .../di/FakeTimelineItemPresenterFactories.kt | 34 +++++++++++++++++++ .../event/TimelineItemEventContentProvider.kt | 4 +++ .../timeline/VoiceMessageStateProvider.kt | 26 ++++++++------ .../libraries/architecture/Presenter.kt | 2 +- ...lineView-D-8_8_null_10,NEXUS_5,1.0,en].png | 4 +-- ...lineView-D-8_8_null_11,NEXUS_5,1.0,en].png | 4 +-- ...lineView-D-8_8_null_12,NEXUS_5,1.0,en].png | 4 +-- ...lineView-D-8_8_null_13,NEXUS_5,1.0,en].png | 3 ++ ...lineView-D-8_8_null_14,NEXUS_5,1.0,en].png | 3 ++ ...lineView-D-8_8_null_15,NEXUS_5,1.0,en].png | 3 ++ ...lineView-D-8_8_null_16,NEXUS_5,1.0,en].png | 3 ++ ...elineView-D-8_8_null_6,NEXUS_5,1.0,en].png | 4 +-- ...elineView-D-8_8_null_7,NEXUS_5,1.0,en].png | 4 +-- ...elineView-D-8_8_null_8,NEXUS_5,1.0,en].png | 4 +-- ...elineView-D-8_8_null_9,NEXUS_5,1.0,en].png | 4 +-- ...lineView-N-8_9_null_10,NEXUS_5,1.0,en].png | 4 +-- ...lineView-N-8_9_null_11,NEXUS_5,1.0,en].png | 4 +-- ...lineView-N-8_9_null_12,NEXUS_5,1.0,en].png | 4 +-- ...lineView-N-8_9_null_13,NEXUS_5,1.0,en].png | 3 ++ ...lineView-N-8_9_null_14,NEXUS_5,1.0,en].png | 3 ++ ...lineView-N-8_9_null_15,NEXUS_5,1.0,en].png | 3 ++ ...lineView-N-8_9_null_16,NEXUS_5,1.0,en].png | 3 ++ ...elineView-N-8_9_null_6,NEXUS_5,1.0,en].png | 4 +-- ...elineView-N-8_9_null_7,NEXUS_5,1.0,en].png | 4 +-- ...elineView-N-8_9_null_8,NEXUS_5,1.0,en].png | 4 +-- ...elineView-N-8_9_null_9,NEXUS_5,1.0,en].png | 4 +-- 27 files changed, 125 insertions(+), 50 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_13,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_14,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_15,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_16,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_13,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_14,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_15,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_16,NEXUS_5,1.0,en].png 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 fdb4b309eb..227067e417 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 @@ -38,6 +38,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -59,6 +60,8 @@ import io.element.android.features.messages.impl.timeline.components.TimelineIte import io.element.android.features.messages.impl.timeline.components.TimelineItemVirtualRow import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator +import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider @@ -333,15 +336,19 @@ internal fun TimelineViewPreview( @PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent ) = ElementPreview { val timelineItems = aTimelineItemList(content) - TimelineView( - state = aTimelineState(timelineItems), - onMessageClicked = {}, - onTimestampClicked = {}, - onUserDataClicked = {}, - onMessageLongClicked = {}, - onReactionClicked = { _, _ -> }, - onReactionLongClicked = { _, _ -> }, - onMoreReactionsClicked = {}, - onSwipeToReply = {}, - ) + CompositionLocalProvider( + LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(), + ) { + TimelineView( + state = aTimelineState(timelineItems), + onMessageClicked = {}, + onTimestampClicked = {}, + onUserDataClicked = {}, + onMessageLongClicked = {}, + onReactionClicked = { _, _ -> }, + onReactionLongClicked = { _, _ -> }, + onMoreReactionsClicked = {}, + onSwipeToReply = {}, + ) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt new file mode 100644 index 0000000000..2823011dbd --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt @@ -0,0 +1,34 @@ +/* + * 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.di + +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState +import io.element.android.features.messages.impl.voicemessages.timeline.aVoiceMessageState +import io.element.android.libraries.architecture.Presenter + +/** + * A fake [TimelineItemPresenterFactories] for screenshot tests. + */ +fun aFakeTimelineItemPresenterFactories() = TimelineItemPresenterFactories( + mapOf( + Pair( + TimelineItemVoiceContent::class.java, + TimelineItemPresenterFactory { Presenter { aVoiceMessageState() } }, + ), + ) +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 4c25bdfb23..bbbd8748a2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -28,8 +28,12 @@ class TimelineItemEventContentProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - VoiceMessageState( + aVoiceMessageState( VoiceMessageState.Button.Downloading, progress = 0f, time = "0:00", - eventSink = {}, ), - VoiceMessageState( + aVoiceMessageState( VoiceMessageState.Button.Retry, progress = 0.5f, time = "0:01", - eventSink = {} ), - VoiceMessageState( + aVoiceMessageState( VoiceMessageState.Button.Play, progress = 1f, time = "1:00", - eventSink = {} ), - VoiceMessageState( + aVoiceMessageState( VoiceMessageState.Button.Pause, progress = 0.2f, time = "10:00", - eventSink = {} ), - VoiceMessageState( + aVoiceMessageState( VoiceMessageState.Button.Disabled, progress = 0.2f, time = "30:00", - eventSink = {} ), ) } + +fun aVoiceMessageState( + button: VoiceMessageState.Button = VoiceMessageState.Button.Play, + progress: Float = 0f, + time: String = "1:00", +) = VoiceMessageState( + button = button, + progress = progress, + time = time, + eventSink = {}, +) diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Presenter.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Presenter.kt index 9bfd089c27..80f60218d9 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Presenter.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Presenter.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.architecture import androidx.compose.runtime.Composable -interface Presenter { +fun interface Presenter { @Composable fun present(): State } diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_10,NEXUS_5,1.0,en].png index 0f72363fc0..039022ff12 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f531805df6b1b7541e1c4d443979cb5434d3ea6c688725debc7f8845abebccf -size 50375 +oid sha256:ee02c20c1ab0b514e12461c95428ae89985ae86fab9e8538eb2301eeaa910603 +size 347767 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_11,NEXUS_5,1.0,en].png index 024e61ae28..3030f83068 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d98aac1065f283b232c96f5a0032865e81bca8816bd235615e131d4bb104a9a5 -size 67474 +oid sha256:1fc3c477b1ce2c74b32cce19bb94c61f49bcf01f751b136c0ff75d573bc66a35 +size 80231 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_12,NEXUS_5,1.0,en].png index 2eb5e3ea2f..7defda84d6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b06025fe43c1cd3f39bcee49222952f3750d0cf7ab2b9281716fcc670ea6934 -size 57499 +oid sha256:7a121baf52bf6997fa6dcfeee9ce77e6d0f3b374227addae4a28305551a6914f +size 53893 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_13,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e1cc82e2af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_13,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c433475dd2eb980e95ef67bf04c2465db1449bf34b3c0eca7f8c6f43b35113e +size 66532 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_14,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0f72363fc0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_14,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f531805df6b1b7541e1c4d443979cb5434d3ea6c688725debc7f8845abebccf +size 50375 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_15,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..024e61ae28 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_15,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d98aac1065f283b232c96f5a0032865e81bca8816bd235615e131d4bb104a9a5 +size 67474 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_16,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2eb5e3ea2f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_16,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b06025fe43c1cd3f39bcee49222952f3750d0cf7ab2b9281716fcc670ea6934 +size 57499 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_6,NEXUS_5,1.0,en].png index 98e3ba6dbe..f0f3f5e927 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f14b44ab65ac39c868de2c28644f886923fb4058f29a0d5b12dca7b6d978238 -size 393377 +oid sha256:eea033f081eb0b62671b3b9c5a626a4d1bfb0b386ff0a8e0011effc40858add3 +size 74258 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_7,NEXUS_5,1.0,en].png index 039022ff12..a5c3e2429a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee02c20c1ab0b514e12461c95428ae89985ae86fab9e8538eb2301eeaa910603 -size 347767 +oid sha256:fc7461871275f11e5ee7abbb6dafaecf7b2c6eac38b3a538ac80b6b722aa0e16 +size 105810 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_8,NEXUS_5,1.0,en].png index 7defda84d6..374f6400ae 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a121baf52bf6997fa6dcfeee9ce77e6d0f3b374227addae4a28305551a6914f -size 53893 +oid sha256:a4dd52ecde13c76639225b482238924485b8cdbbc56f1b270254689f06cae7e8 +size 58016 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_9,NEXUS_5,1.0,en].png index e1cc82e2af..98e3ba6dbe 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-D-8_8_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c433475dd2eb980e95ef67bf04c2465db1449bf34b3c0eca7f8c6f43b35113e -size 66532 +oid sha256:4f14b44ab65ac39c868de2c28644f886923fb4058f29a0d5b12dca7b6d978238 +size 393377 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_10,NEXUS_5,1.0,en].png index e8d97cbf89..99404cf6b2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93482a9053b5efc9331457b907964409e85855e4988358df6a01f1bea7ff1b9f -size 48797 +oid sha256:1b1164272dc31a867c7dbc2bae37ac960168f31da63b233531d509583ea0cea2 +size 178173 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_11,NEXUS_5,1.0,en].png index 462776ac31..f056537e1b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70e0a380cca7ba52a3b695b82c31e68c82c1e03ad6364e6ba73e84f070295528 -size 64748 +oid sha256:eafc87ff92c2d9b1e50bb97251a1bba132afdd6f6ec6769146c3e322ecd95da6 +size 79207 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_12,NEXUS_5,1.0,en].png index c54f9ea050..55f9987532 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bab28935e51e099811884a707858e5f463bb591a8a2766a276044c0dbbae92a1 -size 55244 +oid sha256:bd5f7838b304537265aa2616fd3b96db72346e963a785aa683826870e1a0190a +size 51920 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_13,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..01ed22d8b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_13,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9157f4531a208f2605bfc93314a669d188904b43b2f29bfd154786a50607d8db +size 63928 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_14,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e8d97cbf89 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_14,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93482a9053b5efc9331457b907964409e85855e4988358df6a01f1bea7ff1b9f +size 48797 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_15,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..462776ac31 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_15,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70e0a380cca7ba52a3b695b82c31e68c82c1e03ad6364e6ba73e84f070295528 +size 64748 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_16,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c54f9ea050 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_16,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bab28935e51e099811884a707858e5f463bb591a8a2766a276044c0dbbae92a1 +size 55244 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_6,NEXUS_5,1.0,en].png index 0ba45d341a..ac8e6cf4b4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cf70a69d194c8d297e03ee75be7659d06515c9698751181bdfc8f9da7da6055 -size 189520 +oid sha256:c475d64ef991b3438afd84d8b93d40eedc8060ed862e64523efc0458e6a2a477 +size 71450 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_7,NEXUS_5,1.0,en].png index 99404cf6b2..7aebcca885 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b1164272dc31a867c7dbc2bae37ac960168f31da63b233531d509583ea0cea2 -size 178173 +oid sha256:b1eaf05f8e22a9b22f75d6dd28c1c9c004918f6e85cd60ee0ae245e43affccef +size 99788 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png index 55f9987532..c608f99a68 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd5f7838b304537265aa2616fd3b96db72346e963a785aa683826870e1a0190a -size 51920 +oid sha256:be817a2061d43e210c105b193f1baed6060c51f11d97f45d0593cb40ab7acd98 +size 56403 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_9,NEXUS_5,1.0,en].png index 01ed22d8b0..0ba45d341a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9157f4531a208f2605bfc93314a669d188904b43b2f29bfd154786a50607d8db -size 63928 +oid sha256:5cf70a69d194c8d297e03ee75be7659d06515c9698751181bdfc8f9da7da6055 +size 189520 From cb5b463c9b39a58ec72476f7d78f0f163c19d120 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 27 Oct 2023 12:23:14 +0000 Subject: [PATCH 218/281] Update screenshots --- ..._MessageComposerViewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png | 4 ++-- ..._MessageComposerViewVoice-N-6_7_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...nts_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png | 4 ++-- ...nts_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png | 4 ++-- ...ull_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png | 4 ++-- ...ull_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png index f860d06dd2..3ec6004b6d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-D-6_6_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e250201e94c17c9907721753edf95250626511e20287a16576f0a7388a28bae -size 8123 +oid sha256:3d5209087d7841a80f7213f26aaf8322cc5fee03466b9059e06b0daf5f489c1b +size 10012 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-6_7_null_0,NEXUS_5,1.0,en].png index 946624ce1b..dcddb1ae01 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-6_7_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewVoice-N-6_7_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e566ef57e3db6e25d86dab61492d6acd2712a063f0c70d76b049af6d0b84b41 -size 7750 +oid sha256:5b8fca1de81e424f01a518e3a2547b58c0e9eb7249327f3fcdb1cf62ab655972 +size 9429 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png index 665c8811ac..43f40eb21b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-D-11_11_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b -size 4457 +oid sha256:c8ab81200d74af2e45390223484cf17c6bb488eedf3cb10a425e3e2b7546e1b0 +size 11422 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png index fae8a6fca3..b3d851ea36 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_LiveWaveformView-N-11_12_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 -size 4464 +oid sha256:47fc7fcebbe8c3f8ad745eac3e9687c7722a898e55e1f11f2903cffa2af00636 +size 11059 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png index d4f45d7af7..14ace59009 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-D-17_17_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0db8293a289a2566aa6929bdf2217bde9b73e80accc0a4e396366d4baeab097 -size 6834 +oid sha256:8ee23864465d4f6cd09f4d9c187916793ce4c8fce2ad77a29e2321e1caa57b7d +size 10135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png index 14940aaaca..91de915f81 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessageRecording-N-17_18_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92937df3d489e3c475d61c2ae6c5169a5fc8284607ff5e39c4e7cc6c82bb607b -size 6533 +oid sha256:408ee43f0955bcb120f7e3fbbc51b6276ae139863e1691e6d768a35005d3fa1b +size 9498 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index e3dbc7572c..825b42e9b2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04d22483f11bc6ce290c874b285a36e55144b9ea733b82b0c7fabc1fe9d86772 -size 287 +oid sha256:5abbadadbc4b0943be47d269cddecfe42e8a7ae43e4dd618eb2a8d5aefc7c1b1 +size 27291 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index 949231be20..e32acc4666 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b680969cf395ca6c2626339699c233133aa281ad6275c1d48fbbe03d770a20e2 -size 287 +oid sha256:f73f8eed6746a54bd0bf706acda638155ea99e7aba8b78fc809b9f26b0a5986f +size 24901 From 3482452011a5cee9f907b53aabc04c39430380b7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Oct 2023 14:41:39 +0200 Subject: [PATCH 219/281] Notification: fix issue: clear event only for the matching session. We do not have multi session so the bug is not visible yet. --- .../impl/notifications/DefaultNotificationDrawerManager.kt | 4 ++-- .../push/impl/notifications/NotificationBroadcastReceiver.kt | 2 +- .../push/impl/notifications/NotificationEventQueue.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 318b7cbd49..f2e3240203 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -195,9 +195,9 @@ class DefaultNotificationDrawerManager @Inject constructor( /** * Clear the notifications for a single event. */ - fun clearEvent(eventId: EventId, doRender: Boolean) { + fun clearEvent(sessionId: SessionId, eventId: EventId, doRender: Boolean) { updateEvents(doRender = doRender) { - it.clearEvent(eventId) + it.clearEvent(sessionId, eventId) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt index 8f27d8692c..0fdef871a1 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiver.kt @@ -56,7 +56,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { defaultNotificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId, doRender = false) } actionIds.dismissEvent -> if (eventId != null) { - defaultNotificationDrawerManager.clearEvent(eventId, doRender = false) + defaultNotificationDrawerManager.clearEvent(sessionId, eventId, doRender = false) } actionIds.markRoomRead -> if (roomId != null) { defaultNotificationDrawerManager.clearMessagesForRoom(sessionId, roomId, doRender = true) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt index 6b6730c904..b838f53d6f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt @@ -135,8 +135,8 @@ data class NotificationEventQueue constructor( ) } - fun clearEvent(eventId: EventId) { - queue.removeAll { it.eventId == eventId } + fun clearEvent(sessionId: SessionId, eventId: EventId) { + queue.removeAll { it.sessionId == sessionId && it.eventId == eventId } } fun clearMembershipNotificationForSession(sessionId: SessionId) { From 0e4141863bfd8e662de510e757f7b3710d165f2d Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 14:49:58 +0200 Subject: [PATCH 220/281] Always treat waveform as List (#1663) [MSC3246](https://github.com/matrix-org/matrix-spec-proposals/pull/3246) specifies the waveform as a list of ints because: > Because floating point numbers are not allowed in Matrix events Though DSP on audio data is almost always done using their floating point representation. This PR brings the float<->int rescaling in the `matrix` module so that the application code can always work with float waveform samples. --- .../TimelineItemContentMessageFactory.kt | 3 +- .../impl/voicemessages/WaveformUtils.kt | 28 ------------------- .../composer/VoiceMessageComposerPresenter.kt | 3 +- .../matrix/api/media/AudioDetails.kt | 2 +- .../libraries/matrix/api/room/MatrixRoom.kt | 2 +- .../matrix/impl/media/AudioDetails.kt | 20 +++++++++++-- .../matrix/impl/room/RustMatrixRoom.kt | 5 ++-- .../matrix/test/room/FakeMatrixRoom.kt | 2 +- .../libraries/mediaupload/api/MediaSender.kt | 2 +- .../mediaupload/api/MediaUploadInfo.kt | 2 +- 10 files changed, 28 insertions(+), 41 deletions(-) delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt 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 3262653134..4f78c38393 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 @@ -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.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor import io.element.android.features.messages.impl.timeline.util.toHtmlDocument -import io.element.android.features.messages.impl.voicemessages.fromMSC3246range import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -118,7 +117,7 @@ class TimelineItemContentMessageFactory @Inject constructor( mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, - waveform = messageType.details?.waveform?.fromMSC3246range()?.toImmutableList() ?: persistentListOf(), + waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(), ) else -> TimelineItemAudioContent( body = messageType.body, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt deleted file mode 100644 index 83926f0f22..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/WaveformUtils.kt +++ /dev/null @@ -1,28 +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.voicemessages - -/** - * Resizes the given [0;1024] int list as per unstable MSC3246 spec - * to a [0;1] range float list to be used for waveform rendering. - */ -internal fun List.fromMSC3246range(): List = map { it / 1024f } - -/** - * Resizes the given [0;1] float list to [0;1024] int list as per unstable MSC3246 spec. - */ -internal fun List.toMSC3246range(): List = map { (it * 1024).toInt() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index f3f86a7bf9..557b744d3b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.setValue import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import io.element.android.features.messages.impl.voicemessages.VoiceMessageException -import io.element.android.features.messages.impl.voicemessages.toMSC3246range import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn @@ -219,7 +218,7 @@ class VoiceMessageComposerPresenter @Inject constructor( val result = mediaSender.sendVoiceMessage( uri = file.toUri(), mimeType = mimeType, - waveForm = waveform.toMSC3246range(), + waveForm = waveform, ) if (result.isFailure) { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt index f8cd2d3fb4..059369c5da 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt @@ -20,5 +20,5 @@ import java.time.Duration data class AudioDetails( val duration: Duration, - val waveform: List, + val waveform: List, ) 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 23db1ef00f..1f2de7720f 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 @@ -195,7 +195,7 @@ interface MatrixRoom : Closeable { suspend fun sendVoiceMessage( file: File, audioInfo: AudioInfo, - waveform: List, + waveform: List, progressCallback: ProgressCallback? ): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt index c3fa11e40c..5bca137a85 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt @@ -21,10 +21,26 @@ import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent as RustAudioDet fun RustAudioDetails.map(): AudioDetails = AudioDetails( duration = duration, - waveform = waveform.map { it.toInt() }, + waveform = waveform.fromMSC3246range(), ) fun AudioDetails.map(): RustAudioDetails = RustAudioDetails( duration = duration, - waveform = waveform.map { it.toUShort() } + waveform = waveform.toMSC3246range() ) + +/** + * Resizes the given [0;1024] int list as per unstable MSC3246 spec + * to a [0;1] float list to be used for waveform rendering. + * + * https://github.com/matrix-org/matrix-spec-proposals/blob/travis/msc/audio-waveform/proposals/3246-audio-waveform.md + */ +internal fun List.fromMSC3246range(): List = map { it.toInt() / 1024f } + +/** + * Resizes the given [0;1] float list as per unstable MSC3246 spec + * to a [0;1024] int list to be used for waveform rendering. + * + * https://github.com/matrix-org/matrix-spec-proposals/blob/travis/msc/audio-waveform/proposals/3246-audio-waveform.md + */ +internal fun List.toMSC3246range(): List = map { (it * 1024).toInt().toUShort() } 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 44f095a38c..fa7330b8eb 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 @@ -46,6 +46,7 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.MediaUploadHandlerImpl import io.element.android.libraries.matrix.impl.media.map +import io.element.android.libraries.matrix.impl.media.toMSC3246range import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.poll.toInner import io.element.android.libraries.matrix.impl.room.location.toInner @@ -499,13 +500,13 @@ class RustMatrixRoom( override suspend fun sendVoiceMessage( file: File, audioInfo: AudioInfo, - waveform: List, + waveform: List, progressCallback: ProgressCallback?, ): Result = sendAttachment(listOf(file)) { innerRoom.sendVoiceMessage( url = file.path, audioInfo = audioInfo.map(), - waveform = waveform.map { it.toUShort() }, + waveform = waveform.toMSC3246range(), progressWatcher = progressCallback?.toProgressWatcher(), ) } 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 a2adf0b0bb..244a769895 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 @@ -387,7 +387,7 @@ class FakeMatrixRoom( override suspend fun sendVoiceMessage( file: File, audioInfo: AudioInfo, - waveform: List, + waveform: List, progressCallback: ProgressCallback? ): Result = fakeSendMedia(progressCallback) diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index dde62e7513..dd51198e59 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -55,7 +55,7 @@ class MediaSender @Inject constructor( suspend fun sendVoiceMessage( uri: Uri, mimeType: String, - waveForm: List, + waveForm: List, progressCallback: ProgressCallback? = null ): Result { return preProcessor diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt index e1debf6bda..39c978f625 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt @@ -29,6 +29,6 @@ sealed interface MediaUploadInfo { data class Image(override val file: File, val imageInfo: ImageInfo, val thumbnailFile: File) : MediaUploadInfo data class Video(override val file: File, val videoInfo: VideoInfo, val thumbnailFile: File) : MediaUploadInfo data class Audio(override val file: File, val audioInfo: AudioInfo) : MediaUploadInfo - data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List) : MediaUploadInfo + data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List) : MediaUploadInfo data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo } From cb0abd4c12ccdd59414464eff06f99ce4cd65905 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Oct 2023 14:45:43 +0200 Subject: [PATCH 221/281] Display a single notification for all the fallback notifications. When dismissed, dismiss all the fallback notifications in the model. --- changelog.d/994.bugfix | 1 + .../notifications/NotificationEventQueue.kt | 8 +++++++- .../impl/notifications/NotificationRenderer.kt | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 changelog.d/994.bugfix diff --git a/changelog.d/994.bugfix b/changelog.d/994.bugfix new file mode 100644 index 0000000000..7fe3e828a0 --- /dev/null +++ b/changelog.d/994.bugfix @@ -0,0 +1 @@ +Group fallback notification to avoid having plenty of them displayed. diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt index b838f53d6f..8eea6a9d5a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt @@ -136,7 +136,13 @@ data class NotificationEventQueue constructor( } fun clearEvent(sessionId: SessionId, eventId: EventId) { - queue.removeAll { it.sessionId == sessionId && it.eventId == eventId } + val isFallback = queue.firstOrNull { it.sessionId == sessionId && it.eventId == eventId } is FallbackNotifiableEvent + if (isFallback) { + Timber.d("Removing all the fallbacks") + queue.removeAll { it.sessionId == sessionId && it is FallbackNotifiableEvent } + } else { + queue.removeAll { it.sessionId == sessionId && it.eventId == eventId } + } } fun clearMembershipNotificationForSession(sessionId: SessionId) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index 03241bbdb8..713392c695 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -124,6 +124,7 @@ class NotificationRenderer @Inject constructor( } } + /* fallbackNotifications.forEach { wrapper -> when (wrapper) { is OneShotNotification.Removed -> { @@ -143,6 +144,23 @@ class NotificationRenderer @Inject constructor( } } } + */ + val removedFallback = fallbackNotifications.filterIsInstance() + val appendFallback = fallbackNotifications.filterIsInstance() + if (appendFallback.isEmpty() && removedFallback.isNotEmpty()) { + Timber.tag(loggerTag.value).d("Removing global fallback notification") + notificationDisplayer.cancelNotificationMessage( + tag = "FALLBACK", + id = notificationIdProvider.getFallbackNotificationId(currentUser.userId) + ) + } else if (appendFallback.isNotEmpty()) { + Timber.tag(loggerTag.value).d("Showing fallback notification") + notificationDisplayer.showNotificationMessage( + tag = "FALLBACK", + id = notificationIdProvider.getFallbackNotificationId(currentUser.userId), + notification = appendFallback.first().notification + ) + } // Update summary last to avoid briefly displaying it before other notifications if (summaryNotification is SummaryNotification.Update) { From af513a80236e742026039f511cecfc8a26660d65 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 27 Oct 2023 13:55:42 +0100 Subject: [PATCH 222/281] Fix documentation --- .../designsystem/components/media/WaveformPlaybackView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index f856c0414e..70a506cffa 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -66,7 +66,6 @@ private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F * @param cursorBrush The brush to use to draw the cursor. * @param lineWidth The width of the waveform lines. * @param linePadding The padding between waveform lines. - * @param minimumGraphAmplitude The minimum amplitude to display, regardless of waveform data. */ @OptIn(ExperimentalComposeUiApi::class) @Composable From a07286ace9ec12eb6650ea73180205e370e49353 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 16:02:16 +0200 Subject: [PATCH 223/281] Split `VoiceMessageType` from `AudioMessageType` (#1664) Currently, for compatibility reasons, we implement MSC3245v1 which puts the voice data inside an audio message type. Though at times it seems impractical to deal with a single message type which effectively represents 2 different kinds of messages. This PR creates a new message type called `VoiceMessageType` which is used whenever we receive an event with `"msgtype": "m.audio"` which also has the `"org.matrix.msc3245.voice": {}` field. This makes it easier to process voice messages as different entities throughout the rest of the codebase. --- .../components/TimelineItemEventRow.kt | 21 +++++------ .../TimelineItemContentMessageFactory.kt | 37 ++++++++++++++----- .../impl/DefaultRoomLastMessageFormatter.kt | 10 ++--- .../DefaultRoomLastMessageFormatterTest.kt | 29 +++++---------- .../api/timeline/item/event/MessageType.kt | 7 +++- .../timeline/item/event/EventMessageMapper.kt | 25 +++++++++---- .../notifications/NotifiableEventResolver.kt | 2 + 7 files changed, 76 insertions(+), 55 deletions(-) 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 304ff88662..2c92de4df9 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 @@ -100,6 +100,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType 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.ui.components.AttachmentThumbnail import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType @@ -612,18 +613,14 @@ private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready): Att textContent = messageContent.body, type = AttachmentThumbnailType.Location, ) - is AudioMessageType -> { - when (type.isVoiceMessage) { - true -> AttachmentThumbnailInfo( - textContent = messageContent.body, - type = AttachmentThumbnailType.Voice, - ) - false -> AttachmentThumbnailInfo( - textContent = messageContent.body, - type = AttachmentThumbnailType.Audio, - ) - } - } + is AudioMessageType -> AttachmentThumbnailInfo( + textContent = messageContent.body, + type = AttachmentThumbnailType.Audio, + ) + is VoiceMessageType -> AttachmentThumbnailInfo( + textContent = messageContent.body, + type = AttachmentThumbnailType.Voice, + ) else -> null } } 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 4f78c38393..d15a07526c 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 @@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageT import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import java.time.Duration @@ -110,16 +111,8 @@ class TimelineItemContentMessageFactory @Inject constructor( fileExtension = fileExtensionExtractor.extractFromName(messageType.body) ) } - is AudioMessageType -> when { - featureFlagService.isFeatureEnabled(FeatureFlags.VoiceMessages) && messageType.isVoiceMessage -> TimelineItemVoiceContent( - eventId = eventId, - body = messageType.body, - mediaSource = messageType.source, - duration = messageType.info?.duration ?: Duration.ZERO, - mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, - waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(), - ) - else -> TimelineItemAudioContent( + is AudioMessageType -> { + TimelineItemAudioContent( body = messageType.body, mediaSource = messageType.source, duration = messageType.info?.duration ?: Duration.ZERO, @@ -128,6 +121,30 @@ class TimelineItemContentMessageFactory @Inject constructor( fileExtension = fileExtensionExtractor.extractFromName(messageType.body), ) } + is VoiceMessageType -> { + when (featureFlagService.isFeatureEnabled(FeatureFlags.VoiceMessages)) { + true -> { + TimelineItemVoiceContent( + eventId = eventId, + body = messageType.body, + mediaSource = messageType.source, + duration = messageType.info?.duration ?: Duration.ZERO, + mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, + waveform = messageType.details?.waveform?.toImmutableList() ?: persistentListOf(), + ) + } + false -> { + TimelineItemAudioContent( + body = messageType.body, + mediaSource = messageType.source, + duration = messageType.info?.duration ?: Duration.ZERO, + mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, + formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), + fileExtension = fileExtensionExtractor.extractFromName(messageType.body), + ) + } + } + } is FileMessageType -> { val fileExtension = fileExtensionExtractor.extractFromName(messageType.body) TimelineItemFileContent( 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 99b06401d9..96626e8633 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 @@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType 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.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject @@ -128,11 +129,10 @@ class DefaultRoomLastMessageFormatter @Inject constructor( sp.getString(CommonStrings.common_file) } is AudioMessageType -> { - if (messageType.isVoiceMessage) { - sp.getString(CommonStrings.common_voice_message) - } else { - sp.getString(CommonStrings.common_audio) - } + sp.getString(CommonStrings.common_audio) + } + is VoiceMessageType -> { + sp.getString(CommonStrings.common_voice_message) } is OtherMessageType -> { messageType.body 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 48c498c3d8..e9d91fbb8c 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 @@ -47,6 +47,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType 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.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.aPollContent @@ -162,8 +163,8 @@ class DefaultRoomLastMessageFormatterTest { val sharedContentMessagesTypes = arrayOf( TextMessageType(body, null), VideoMessageType(body, MediaSource("url"), null), - AudioMessageType(body, MediaSource("url"), null, null, false), - AudioMessageType(body, MediaSource("url"), null, null, true), + AudioMessageType(body, MediaSource("url"), null), + VoiceMessageType(body, MediaSource("url"), null, null), ImageMessageType(body, MediaSource("url"), null), FileMessageType(body, MediaSource("url"), null), LocationMessageType(body, "geo:1,2", null), @@ -199,12 +200,8 @@ class DefaultRoomLastMessageFormatterTest { for ((type, result) in resultsInDm) { val expectedResult = when (type) { is VideoMessageType -> "Video" - is AudioMessageType -> { - when (type.isVoiceMessage) { - true -> "Voice message" - false -> "Audio" - } - } + is AudioMessageType -> "Audio" + is VoiceMessageType -> "Voice message" is ImageMessageType -> "Image" is FileMessageType -> "File" is LocationMessageType -> "Shared location" @@ -222,12 +219,8 @@ class DefaultRoomLastMessageFormatterTest { val string = result.toString() val expectedResult = when (type) { is VideoMessageType -> "$senderName: Video" - is AudioMessageType -> { - when (type.isVoiceMessage) { - true -> "$senderName: Voice message" - false -> "$senderName: Audio" - } - } + is AudioMessageType -> "$senderName: Audio" + is VoiceMessageType -> "$senderName: Voice message" is ImageMessageType -> "$senderName: Image" is FileMessageType -> "$senderName: File" is LocationMessageType -> "$senderName: Shared location" @@ -239,12 +232,8 @@ class DefaultRoomLastMessageFormatterTest { } val shouldCreateAnnotatedString = when (type) { is VideoMessageType -> true - is AudioMessageType -> { - when (type.isVoiceMessage) { - true -> true - false -> true - } - } + is AudioMessageType -> true + is VoiceMessageType -> true is ImageMessageType -> true is FileMessageType -> true is LocationMessageType -> false diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt index 09f0c00a7c..c8122935bd 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt @@ -48,8 +48,13 @@ data class AudioMessageType( val body: String, val source: MediaSource, val info: AudioInfo?, +) : MessageType + +data class VoiceMessageType( + val body: String, + val source: MediaSource, + val info: AudioInfo?, val details: AudioDetails?, - val isVoiceMessage: Boolean, ) : MessageType data class VideoMessageType( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 521ff3bd5a..7f88cc0569 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageT import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType 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.impl.media.map import org.matrix.rustcomponents.sdk.Message import org.matrix.rustcomponents.sdk.MessageType @@ -77,13 +78,23 @@ class EventMessageMapper { fun mapMessageType(type: RustMessageType?) = when (type) { is RustMessageType.Audio -> { - AudioMessageType( - body = type.content.body, - source = type.content.source.map(), - info = type.content.info?.map(), - details = type.content.audio?.map(), - isVoiceMessage = type.content.voice != null, - ) + when (type.content.voice) { + null -> { + AudioMessageType( + body = type.content.body, + source = type.content.source.map(), + info = type.content.info?.map(), + ) + } + else -> { + VoiceMessageType( + body = type.content.body, + source = type.content.source.map(), + info = type.content.info?.map(), + details = type.content.audio?.map(), + ) + } + } } is RustMessageType.File -> { FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) 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 aabc58befb..c8d496a64e 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 @@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageT import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType 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.push.impl.R import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent @@ -210,6 +211,7 @@ class NotifiableEventResolver @Inject constructor( ): String { return when (val messageType = content.messageType) { is AudioMessageType -> messageType.body + is VoiceMessageType -> messageType.body is EmoteMessageType -> "* $senderDisplayName ${messageType.body}" is FileMessageType -> messageType.body is ImageMessageType -> messageType.body From 7ebea4acf13fb7acb63f7f4a29d0515764837599 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 27 Oct 2023 15:07:02 +0100 Subject: [PATCH 224/281] Refactor waveform factory function --- .../components/media/FakeWaveformFactory.kt | 24 +++++++++---------- .../libraries/textcomposer/TextComposer.kt | 2 +- .../components/VoiceMessagePreview.kt | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt index 19b2d4e8b5..4273d95e2c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/FakeWaveformFactory.kt @@ -20,17 +20,15 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlin.random.Random -object FakeWaveformFactory { - /** - * Generate a waveform for testing purposes. - * - * The waveform is a list of floats between 0 and 1. - * - * @param length The length of the waveform. - */ - fun createFakeWaveform(length: Int = 1000): ImmutableList { - val random = Random(seed = 2) - return List(length) { random.nextFloat() } - .toPersistentList() - } +/** + * Generate a waveform for testing purposes. + * + * The waveform is a list of floats between 0 and 1. + * + * @param length The length of the waveform. + */ +fun createFakeWaveform(length: Int = 1000): ImmutableList { + val random = Random(seed = 2) + return List(length) { random.nextFloat() } + .toPersistentList() } 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 3116f79ba0..5c55dbb517 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 @@ -48,7 +48,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.media.FakeWaveformFactory.createFakeWaveform +import io.element.android.libraries.designsystem.components.media.createFakeWaveform import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleUp diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index be06f853f8..baee7d1be5 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.media.FakeWaveformFactory.createFakeWaveform import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView +import io.element.android.libraries.designsystem.components.media.createFakeWaveform import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.applyScaleUp From 71644910e8d02a3265055fcc30af6fc060047be8 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 27 Oct 2023 15:14:01 +0100 Subject: [PATCH 225/281] Ensure deleting pauses audio --- .../composer/VoiceMessageComposerPresenter.kt | 5 ++++- .../VoiceMessageComposerPresenterTest.kt | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index 0fae36f1ec..3ea8bde6ca 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -165,7 +165,10 @@ class VoiceMessageComposerPresenter @Inject constructor( is VoiceMessageComposerEvents.SendVoiceMessage -> localCoroutineScope.launch { onSendButtonPress() } - VoiceMessageComposerEvents.DeleteVoiceMessage -> localCoroutineScope.deleteRecording() + VoiceMessageComposerEvents.DeleteVoiceMessage -> { + player.pause() + localCoroutineScope.deleteRecording() + } VoiceMessageComposerEvents.DismissPermissionsRationale -> onDismissPermissionsRationale() VoiceMessageComposerEvents.AcceptPermissionRationale -> onAcceptPermissionsRationale() is VoiceMessageComposerEvents.LifecycleEvent -> onLifecycleEvent(event.event) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index d3cb53fb49..67cf2123be 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -209,6 +209,28 @@ class VoiceMessageComposerPresenterTest { } } + @Test + fun `present - delete while playing`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) + awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) + awaitItem().apply { + assertThat(voiceMessageState).isEqualTo(aPreviewState(isPlaying = false)) + } + + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + + testPauseAndDestroy(finalState) + } + } + @Test fun `present - send recording`() = runTest { val presenter = createVoiceMessageComposerPresenter() From 08ba8e182a0cebe5a783f3fa88e29ea06f30c3e4 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 27 Oct 2023 15:16:58 +0100 Subject: [PATCH 226/281] Ensure sending pauses audio --- .../composer/VoiceMessageComposerPresenter.kt | 1 + .../VoiceMessageComposerPresenterTest.kt | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index 3ea8bde6ca..ff3e2df722 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -149,6 +149,7 @@ class VoiceMessageComposerPresenter @Inject constructor( return@lambda } isSending = true + player.pause() appCoroutineScope.sendMessage( file = finishedState.file, mimeType = finishedState.mimeType, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 67cf2123be..4cffe65036 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -251,6 +251,29 @@ class VoiceMessageComposerPresenterTest { } } + @Test + fun `present - send while playing`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) + awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) + assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState( + isSending = true, isPlaying = false, + )) + + val finalState = awaitItem() + assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) + assertThat(matrixRoom.sendMediaCount).isEqualTo(1) + voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + + testPauseAndDestroy(finalState) + } + } + @Test fun `present - send recording before previous completed, waits`() = runTest { val presenter = createVoiceMessageComposerPresenter() From ed91930a97b36f67266d4b6b81ea8e47338da792 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 17:10:56 +0200 Subject: [PATCH 227/281] Enable swipe to reply on voice messages (#1668) With the new waveform view it does not conflict anymore. --- .../impl/timeline/model/event/TimelineItemEventContent.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt index eda9bcbdb4..56c0b63b0e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt @@ -40,7 +40,6 @@ fun TimelineItemEventContent.canBeCopied(): Boolean = */ fun TimelineItemEventContent.canBeRepliedTo(): Boolean = when (this) { - is TimelineItemVoiceContent, // TODO Voice messages: swipe to reply disabled for now to avoid conflict with audio scrubbing. is TimelineItemRedactedContent, is TimelineItemStateContent, is TimelineItemPollContent -> false From c3cbf4de96db2ea0622cde99d70afa4477fb7cd8 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 17:16:32 +0200 Subject: [PATCH 228/281] Default enable voice message feature flag (#1669) :party --- .../element/android/libraries/featureflag/api/FeatureFlags.kt | 2 +- .../libraries/featureflag/impl/StaticFeatureFlagProvider.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 819679082b..22daf73d69 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 @@ -47,7 +47,7 @@ enum class FeatureFlags( key = "feature.voicemessages", title = "Voice messages", description = "Send and receive voice messages", - defaultValue = false, + defaultValue = true, ), PinUnlock( key = "feature.pinunlock", 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 7ff59c7512..27ce0911d4 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 @@ -35,7 +35,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.LocationSharing -> true FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> true - FeatureFlags.VoiceMessages -> false + FeatureFlags.VoiceMessages -> true FeatureFlags.PinUnlock -> false FeatureFlags.InRoomCalls -> false FeatureFlags.Mentions -> false From 81122ec33b4b4b4f25a66188e350101a115c666c Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 17:23:53 +0200 Subject: [PATCH 229/281] Track errors in VoiceMessagePresenter (#1667) Story: https://github.com/vector-im/element-meta/issues/2085 --- .../impl/voicemessages/VoiceMessageException.kt | 3 +++ .../timeline/VoiceMessagePresenter.kt | 16 +++++++++++++++- .../timeline/VoiceMessagePresenterTest.kt | 10 ++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt index 2020b687ae..7ca95ad9a7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt @@ -23,4 +23,7 @@ internal sealed class VoiceMessageException : Exception() { data class PermissionMissing( override val message: String?, override val cause: Throwable? ) : VoiceMessageException() + data class PlayMessageError( + override val message: String?, override val cause: Throwable? + ) : VoiceMessageException() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt index 6bff6ef26f..1bbdedc22d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt @@ -33,11 +33,13 @@ import dagger.multibindings.IntoMap import io.element.android.features.messages.impl.timeline.di.TimelineItemEventContentKey import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.di.RoomScope import io.element.android.libraries.ui.utils.time.formatShort +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.milliseconds @@ -52,6 +54,7 @@ interface VoiceMessagePresenterModule { class VoiceMessagePresenter @AssistedInject constructor( voiceMessagePlayerFactory: VoiceMessagePlayer.Factory, + private val analyticsService: AnalyticsService, @Assisted private val content: TimelineItemVoiceContent, ) : Presenter { @@ -102,7 +105,18 @@ class VoiceMessagePresenter @AssistedInject constructor( if (playerState.isPlaying) { player.pause() } else { - scope.launch { play.runUpdatingState { player.play() } } + scope.launch { + play.runUpdatingState( + errorTransform = { + analyticsService.trackError( + VoiceMessageException.PlayMessageError("Error while trying to play voice message", it) + ) + it + }, + ) { + player.play() + } + } } } is VoiceMessageEvents.Seek -> { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt index e735dd4f28..257174ec8d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/timeline/VoiceMessagePresenterTest.kt @@ -22,12 +22,15 @@ import app.cash.turbine.test import com.google.common.truth.Truth import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent +import io.element.android.features.messages.impl.voicemessages.VoiceMessageException import io.element.android.features.messages.impl.voicemessages.timeline.DefaultVoiceMessagePlayer import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageMediaRepo import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessagePresenter import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer +import io.element.android.services.analytics.api.AnalyticsService +import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.test.runTest import org.junit.Test @@ -77,8 +80,10 @@ class VoiceMessagePresenterTest { @Test fun `pressing play downloads and fails`() = runTest { + val analyticsService = FakeAnalyticsService() val presenter = createVoiceMessagePresenter( voiceMessageMediaRepo = FakeVoiceMessageMediaRepo().apply { shouldFail = true }, + analyticsService = analyticsService, content = aTimelineItemVoiceContent(durationMs = 2_000), ) moleculeFlow(RecompositionMode.Immediate) { @@ -102,6 +107,9 @@ class VoiceMessagePresenterTest { Truth.assertThat(it.progress).isEqualTo(0f) Truth.assertThat(it.time).isEqualTo("0:02") } + analyticsService.trackedErrors.first().also { + Truth.assertThat(it).isInstanceOf(VoiceMessageException.PlayMessageError::class.java) + } } } @@ -190,6 +198,7 @@ class VoiceMessagePresenterTest { fun createVoiceMessagePresenter( voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(), + analyticsService: AnalyticsService = FakeAnalyticsService(), content: TimelineItemVoiceContent = aTimelineItemVoiceContent(), ) = VoiceMessagePresenter( voiceMessagePlayerFactory = { eventId, mediaSource, mimeType, body -> @@ -202,5 +211,6 @@ fun createVoiceMessagePresenter( body = body ) }, + analyticsService = analyticsService, content = content, ) From 99fbb1919d1b1e476e4d79fd14ede651afc0b640 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Oct 2023 18:20:59 +0200 Subject: [PATCH 230/281] Add topBar parameter to HeaderFooterPage and use a Scaffold. --- .../atomic/pages/HeaderFooterPage.kt | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) 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 8d9718283e..5af89a600e 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 @@ -18,21 +18,23 @@ package io.element.android.libraries.designsystem.atomic.pages import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.theme.ElementTheme /** * @param modifier Classical modifier. + * @param topBar optional topBar. * @param header optional header. * @param footer optional footer. * @param content main content. @@ -40,28 +42,34 @@ import io.element.android.libraries.theme.ElementTheme @Composable fun HeaderFooterPage( modifier: Modifier = Modifier, + topBar: @Composable () -> Unit = {}, header: @Composable () -> Unit = {}, footer: @Composable () -> Unit = {}, content: @Composable () -> Unit = {}, ) { - Column( - modifier = modifier - .fillMaxSize() - .systemBarsPadding() - .padding(all = 20.dp), - ) { - // Header - header() - // Content + Scaffold( + modifier = modifier, + topBar = topBar, + ) { padding -> Column( modifier = Modifier - .weight(1f) - .fillMaxWidth(), + .padding(padding) + .consumeWindowInsets(padding) + .padding(all = 20.dp), ) { - content() + // Header + header() + // Content + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + content() + } + // Footer + footer() } - // Footer - footer() } } From f3c4c6218e1c3f0d0a3e5f9634906499597ec1d3 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 18:24:16 +0200 Subject: [PATCH 231/281] Use correct icon tint in voice message preview player (#1672) Action item from design review. --- .../libraries/textcomposer/components/VoiceMessagePreview.kt | 2 ++ ..._null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png | 4 ++-- ..._null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index baee7d1be5..8375a43642 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -129,12 +129,14 @@ private fun PlayerButton( private fun PauseIcon() = Icon( resourceId = R.drawable.ic_pause, contentDescription = stringResource(id = CommonStrings.a11y_pause), + tint = ElementTheme.colors.iconSecondary, ) @Composable private fun PlayIcon() = Icon( resourceId = R.drawable.ic_play, contentDescription = stringResource(id = CommonStrings.a11y_play), + tint = ElementTheme.colors.iconSecondary, ) @PreviewsDayNight diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png index 37e1350a65..a7c99817a0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff4763716e5283a13f7028532cf22953f08d93512bd19ac98e774fd950173ad5 -size 22534 +oid sha256:a53da79f9919d7f55884273fdcc473443a2d2a8eac36bc1e7c79baecff40b69e +size 22526 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png index 32be676e9e..479748f1f6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a88306846e0bf2958e235c5b0944376f4c4983ceb026b4da962deb10b249f315 -size 19326 +oid sha256:195cb0e133075a7c8d2b429c32337dd62a7ce97ede7c0a62b630f03eaa145f63 +size 19293 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index 825b42e9b2..889aa92b7d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5abbadadbc4b0943be47d269cddecfe42e8a7ae43e4dd618eb2a8d5aefc7c1b1 -size 27291 +oid sha256:d93f97e1ae36edc6375b00de4421166f8012ab16cc6629897b6a36b062cbbff6 +size 27255 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index e32acc4666..ffcc9c6d02 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f73f8eed6746a54bd0bf706acda638155ea99e7aba8b78fc809b9f26b0a5986f -size 24901 +oid sha256:b34b9934ea8c06dc98b080d6dcd07c2c12d95c0841c1fd09e2ebc62bb8df2fce +size 24904 From 21499a2d40c7ef725c9f1eaee7625a35d52f828e Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 18:45:33 +0200 Subject: [PATCH 232/281] Create 1669.feature (#1674) Add changelog item for voice messages --- changelog.d/1669.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1669.feature diff --git a/changelog.d/1669.feature b/changelog.d/1669.feature new file mode 100644 index 0000000000..12f974eea2 --- /dev/null +++ b/changelog.d/1669.feature @@ -0,0 +1 @@ +Enable voice messages for all users From 8121d1a6dececcd9068962a46d671c83030ca79d Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 27 Oct 2023 21:43:52 +0100 Subject: [PATCH 233/281] Show voice message preview player progress (#1675) * Show voice message preview player progress * Update screenshots * Fix test * Some nits over mediaplayer stuff --------- Co-authored-by: ElementBot Co-authored-by: Marco Romano --- .../composer/VoiceMessageComposerPlayer.kt | 14 +++++- .../composer/VoiceMessageComposerPresenter.kt | 1 + .../VoiceMessageComposerPresenterTest.kt | 12 +++-- .../components/media/WaveformPlaybackView.kt | 46 +++++++++++-------- .../libraries/mediaplayer/api/MediaPlayer.kt | 4 ++ .../mediaplayer/impl/MediaPlayerImpl.kt | 4 +- .../mediaplayer/impl/SimplePlayer.kt | 3 ++ .../mediaplayer/test/FakeMediaPlayer.kt | 13 ++++-- .../libraries/textcomposer/TextComposer.kt | 36 ++++++++++++--- .../components/VoiceMessagePreview.kt | 1 + .../textcomposer/model/VoiceMessageState.kt | 1 + ...mposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 +- ...mposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 +- 13 files changed, 102 insertions(+), 41 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt index 36dddb7d0d..b166dcd78a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt @@ -41,7 +41,8 @@ class VoiceMessageComposerPlayer @Inject constructor( State( isPlaying = state.isPlaying, - currentPosition = state.currentPosition + currentPosition = state.currentPosition, + duration = state.duration, ) }.distinctUntilChanged() @@ -82,12 +83,23 @@ class VoiceMessageComposerPlayer @Inject constructor( * The elapsed time of this player in milliseconds. */ val currentPosition: Long, + /** + * The duration of this player in milliseconds. + */ + val duration: Long, ) { companion object { val NotPlaying = State( isPlaying = false, currentPosition = 0L, + duration = 0L, ) } + + /** + * The progress of this player between 0 and 1. + */ + val progress: Float = + if (duration <= currentPosition) 0f else currentPosition.toFloat() / duration.toFloat() } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index ff3e2df722..c8edec15a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -185,6 +185,7 @@ class VoiceMessageComposerPresenter @Inject constructor( is VoiceRecorderState.Finished -> VoiceMessageState.Preview( isSending = isSending, isPlaying = isPlaying, + playbackProgress = playerState.progress, waveform = waveform, ) else -> VoiceMessageState.Idle diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 4cffe65036..c0d1723c36 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -164,7 +164,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) val finalState = awaitItem().also { - assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = true)) + assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = true, playbackProgress = 0.1f)) } voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) @@ -183,7 +183,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause)) val finalState = awaitItem().also { - assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = false)) + assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f)) } voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) @@ -220,7 +220,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage) awaitItem().apply { - assertThat(voiceMessageState).isEqualTo(aPreviewState(isPlaying = false)) + assertThat(voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f)) } val finalState = awaitItem() @@ -262,7 +262,7 @@ class VoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play)) awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage) assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState( - isSending = true, isPlaying = false, + isSending = true, isPlaying = false, playbackProgress = 0.1f )) val finalState = awaitItem() @@ -510,7 +510,7 @@ class VoiceMessageComposerPresenterTest { is VoiceMessageState.Preview -> when (state.isPlaying) { // If the preview was playing, it pauses true -> awaitItem().apply { - assertThat(voiceMessageState).isEqualTo(aPreviewState()) + assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.1f)) } false -> mostRecentState } @@ -561,10 +561,12 @@ class VoiceMessageComposerPresenterTest { private fun aPreviewState( isPlaying: Boolean = false, + playbackProgress: Float = 0f, isSending: Boolean = false, waveform: List = voiceRecorder.waveform, ) = VoiceMessageState.Preview( isPlaying = isPlaying, + playbackProgress = playbackProgress, isSending = isSending, waveform = waveform.toImmutableList(), ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt index 70a506cffa..bae772c0f2 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt @@ -60,6 +60,7 @@ private const val DEFAULT_GRAPHICS_LAYER_ALPHA: Float = 0.99F * @param showCursor Whether to show the cursor or not. * @param waveform The waveform to display. Use [FakeWaveformFactory] to generate a fake waveform. * @param modifier The modifier to be applied to the view. + * @param seekEnabled Whether the user can seek the waveform or not. * @param onSeek Callback when the user seeks the waveform. Called with a value between 0 and 1. * @param brush The brush to use to draw the waveform. * @param progressBrush The brush to use to draw the progress. @@ -74,6 +75,7 @@ fun WaveformPlaybackView( showCursor: Boolean, waveform: ImmutableList, modifier: Modifier = Modifier, + seekEnabled: Boolean = true, onSeek: (progress: Float) -> Unit = {}, brush: Brush = SolidColor(ElementTheme.colors.iconQuaternary), progressBrush: Brush = SolidColor(ElementTheme.colors.iconSecondary), @@ -106,28 +108,32 @@ fun WaveformPlaybackView( modifier = Modifier .fillMaxWidth() .graphicsLayer(alpha = DEFAULT_GRAPHICS_LAYER_ALPHA) - .pointerInteropFilter(requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent) { - return@pointerInteropFilter when (it.action) { - MotionEvent.ACTION_DOWN -> { - if (it.x in 0F..canvasSizePx.width) { - requestDisallowInterceptTouchEvent.invoke(true) - seekProgress.value = it.x / canvasSizePx.width - true - } else false - } - MotionEvent.ACTION_MOVE -> { - if (it.x in 0F..canvasSizePx.width) { - seekProgress.value = it.x / canvasSizePx.width + .let { + if (!seekEnabled) return@let it + + it.pointerInteropFilter(requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent) { e -> + return@pointerInteropFilter when (e.action) { + MotionEvent.ACTION_DOWN -> { + if (e.x in 0F..canvasSizePx.width) { + requestDisallowInterceptTouchEvent.invoke(true) + seekProgress.value = e.x / canvasSizePx.width + true + } else false } - true + MotionEvent.ACTION_MOVE -> { + if (e.x in 0F..canvasSizePx.width) { + seekProgress.value = e.x / canvasSizePx.width + } + true + } + MotionEvent.ACTION_UP -> { + requestDisallowInterceptTouchEvent.invoke(false) + seekProgress.value?.let(onSeek) + seekProgress.value = null + true + } + else -> false } - MotionEvent.ACTION_UP -> { - requestDisallowInterceptTouchEvent.invoke(false) - seekProgress.value?.let(onSeek) - seekProgress.value = null - true - } - else -> false } } .then(modifier) diff --git a/libraries/mediaplayer/api/src/main/kotlin/io/element/android/libraries/mediaplayer/api/MediaPlayer.kt b/libraries/mediaplayer/api/src/main/kotlin/io/element/android/libraries/mediaplayer/api/MediaPlayer.kt index 2c72cbf54a..9c74e27daa 100644 --- a/libraries/mediaplayer/api/src/main/kotlin/io/element/android/libraries/mediaplayer/api/MediaPlayer.kt +++ b/libraries/mediaplayer/api/src/main/kotlin/io/element/android/libraries/mediaplayer/api/MediaPlayer.kt @@ -74,5 +74,9 @@ interface MediaPlayer : AutoCloseable { * The current position of the player. */ val currentPosition: Long, + /** + * The duration of the current content. + */ + val duration: Long, ) } diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImpl.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImpl.kt index 88e592a9d8..0e46ad0fee 100644 --- a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImpl.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/MediaPlayerImpl.kt @@ -47,6 +47,7 @@ class MediaPlayerImpl @Inject constructor( _state.update { it.copy( currentPosition = player.currentPosition, + duration = player.duration.coerceAtLeast(0), isPlaying = isPlaying, ) } @@ -61,6 +62,7 @@ class MediaPlayerImpl @Inject constructor( _state.update { it.copy( currentPosition = player.currentPosition, + duration = player.duration.coerceAtLeast(0), mediaId = mediaItem?.mediaId, ) } @@ -74,7 +76,7 @@ class MediaPlayerImpl @Inject constructor( private val scope = CoroutineScope(Job() + Dispatchers.Main) private var job: Job? = null - private val _state = MutableStateFlow(MediaPlayer.State(false, null, 0L)) + private val _state = MutableStateFlow(MediaPlayer.State(false, null, 0L, 0L)) override val state: StateFlow = _state.asStateFlow() diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt index 79a51973e6..8ae4325e64 100644 --- a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt @@ -33,6 +33,7 @@ interface SimplePlayer { fun addListener(listener: Listener) val currentPosition: Long val playbackState: Int + val duration: Long fun clearMediaItems() fun setMediaItem(mediaItem: MediaItem) fun getCurrentMediaItem(): MediaItem? @@ -73,6 +74,8 @@ class SimplePlayerImpl( get() = p.currentPosition override val playbackState: Int get() = p.playbackState + override val duration: Long + get() = p.duration override fun clearMediaItems() = p.clearMediaItems() diff --git a/libraries/mediaplayer/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeMediaPlayer.kt b/libraries/mediaplayer/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeMediaPlayer.kt index efa5bb1fea..c94261f27e 100644 --- a/libraries/mediaplayer/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeMediaPlayer.kt +++ b/libraries/mediaplayer/test/src/main/kotlin/io/element/android/libraries/mediaplayer/test/FakeMediaPlayer.kt @@ -26,7 +26,12 @@ import kotlinx.coroutines.flow.update * Fake implementation of [MediaPlayer] for testing purposes. */ class FakeMediaPlayer : MediaPlayer { - private val _state = MutableStateFlow(MediaPlayer.State(false, null, 0L)) + companion object { + private const val FAKE_TOTAL_DURATION_MS = 10_000L + private const val FAKE_PLAYED_DURATION_MS = 1000L + } + + private val _state = MutableStateFlow(MediaPlayer.State(false, null, 0L, 0L)) override val state: StateFlow = _state.asStateFlow() @@ -35,7 +40,8 @@ class FakeMediaPlayer : MediaPlayer { it.copy( isPlaying = true, mediaId = mediaId, - currentPosition = it.currentPosition + 1000L, + currentPosition = it.currentPosition + FAKE_PLAYED_DURATION_MS, + duration = FAKE_TOTAL_DURATION_MS, ) } } @@ -44,7 +50,8 @@ class FakeMediaPlayer : MediaPlayer { _state.update { it.copy( isPlaying = true, - currentPosition = it.currentPosition + 1000L, + currentPosition = it.currentPosition + FAKE_PLAYED_DURATION_MS, + duration = FAKE_TOTAL_DURATION_MS, ) } } 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 5c55dbb517..a9236c6bfd 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 @@ -76,8 +76,8 @@ import io.element.android.libraries.textcomposer.components.textInputRoundedCorn import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.PressEvent -import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.Suggestion +import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -86,8 +86,8 @@ import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlin.time.Duration.Companion.seconds import uniffi.wysiwyg_composer.MenuAction +import kotlin.time.Duration.Companion.seconds @Composable fun TextComposer( @@ -194,7 +194,7 @@ fun TextComposer( when (voiceMessageState) { VoiceMessageState.Idle, is VoiceMessageState.Recording -> recordVoiceButton - is VoiceMessageState.Preview -> when(voiceMessageState.isSending) { + is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { true -> uploadVoiceProgress false -> sendVoiceButton } @@ -210,6 +210,7 @@ fun TextComposer( isInteractive = !voiceMessageState.isSending, isPlaying = voiceMessageState.isPlaying, waveform = voiceMessageState.waveform, + playbackProgress = voiceMessageState.playbackProgress, onPlayClick = onPlayVoiceMessageClicked, onPauseClick = onPauseVoiceMessageClicked, onSeek = onSeekVoiceMessage, @@ -221,7 +222,7 @@ fun TextComposer( } val voiceDeleteButton = @Composable { - if(voiceMessageState is VoiceMessageState.Preview) { + if (voiceMessageState is VoiceMessageState.Preview) { VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending, onClick = onDeleteVoiceMessage) } } @@ -817,11 +818,32 @@ internal fun TextComposerVoicePreview() = ElementPreview { PreviewColumn(items = persistentListOf({ VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, List(100) { it.toFloat() / 100 }.toPersistentList())) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = false, isPlaying = false, waveform = createFakeWaveform())) + VoicePreview( + voiceMessageState = VoiceMessageState.Preview( + isSending = false, + isPlaying = false, + waveform = createFakeWaveform(), + playbackProgress = 0.0f + ) + ) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = false, isPlaying = true, waveform = createFakeWaveform())) + VoicePreview( + voiceMessageState = VoiceMessageState.Preview( + isSending = false, + isPlaying = true, + waveform = createFakeWaveform(), + playbackProgress = 0.2f + ) + ) }, { - VoicePreview(voiceMessageState = VoiceMessageState.Preview(isSending = true, isPlaying = false, waveform = createFakeWaveform())) + VoicePreview( + voiceMessageState = VoiceMessageState.Preview( + isSending = true, + isPlaying = false, + waveform = createFakeWaveform(), + playbackProgress = 0.0f + ) + ) })) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 8375a43642..5a4c1bd49e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -95,6 +95,7 @@ internal fun VoiceMessagePreview( playbackProgress = playbackProgress, showCursor = isInteractive, waveform = waveform, + seekEnabled = false, // TODO enable seeking onSeek = onSeek, ) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt index 5a3e030c56..b3798be203 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt @@ -25,6 +25,7 @@ sealed class VoiceMessageState { data class Preview( val isSending: Boolean, val isPlaying: Boolean, + val playbackProgress: Float, val waveform: ImmutableList, ): VoiceMessageState() diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index 889aa92b7d..433aeb1bca 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d93f97e1ae36edc6375b00de4421166f8012ab16cc6629897b6a36b062cbbff6 -size 27255 +oid sha256:e2fe52c3e34e58ab38f7fa0b4b2281bdd8ef269acb3426449faa93fe7b434feb +size 27393 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index ffcc9c6d02..7bd842a1c4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b34b9934ea8c06dc98b080d6dcd07c2c12d95c0841c1fd09e2ebc62bb8df2fce -size 24904 +oid sha256:ffa7daeaf5e27170395307c87c68a7674fb115bb9806cfc0d438a905571ce37f +size 25100 From a51fdf9fb853983ec1c59d28a8802dee6320e3da Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 23:12:43 +0200 Subject: [PATCH 234/281] Blink red dot while recording voice message (#1677) Story: https://github.com/vector-im/element-meta/issues/2084 --- .../components/VoiceMessageRecording.kt | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 6930441e4b..608a20200d 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -16,6 +16,11 @@ package io.element.android.libraries.textcomposer.components +import androidx.compose.animation.core.InfiniteRepeatableSpec +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.TweenSpec +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -28,8 +33,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -83,11 +90,24 @@ internal fun VoiceMessageRecording( @Composable private fun RedRecordingDot( modifier: Modifier = Modifier, -) = Box( - modifier = modifier - .size(8.dp) - .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) -) +) { + val infiniteTransition = rememberInfiniteTransition("RedRecordingDot") + val alpha by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 0f, + animationSpec = InfiniteRepeatableSpec( + animation = TweenSpec(durationMillis = 1_000), + repeatMode = RepeatMode.Reverse, + ), + label = "RedRecordingDotAlpha", + ) + Box( + modifier = modifier + .size(8.dp) + .alpha(alpha) + .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) + ) +} @PreviewsDayNight @Composable From 563aaa6102655a5358b617fffa392a4a3af78369 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 27 Oct 2023 23:57:05 +0200 Subject: [PATCH 235/281] Don't show body in replies to voice messages but rather show "Voice message" string (#1673) Story: https://github.com/vector-im/element-meta/issues/2106 --- .../android/features/messages/impl/MessagesPresenter.kt | 2 +- .../features/messages/impl/actionlist/ActionListView.kt | 2 +- .../messages/impl/timeline/components/TimelineItemEventRow.kt | 2 +- ...ionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...ionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) 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 f924e7f716..fdfd1484d5 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 @@ -332,7 +332,7 @@ class MessagesPresenter @AssistedInject constructor( type = AttachmentThumbnailType.Audio, ) is TimelineItemVoiceContent -> AttachmentThumbnailInfo( - textContent = targetEvent.content.body, + textContent = textContent, type = AttachmentThumbnailType.Voice, ) is TimelineItemLocationContent -> AttachmentThumbnailInfo( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index bed3af59bd..c8bdba9e9d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -318,7 +318,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif ) ) } - content = { ContentForBody(event.content.body) } + content = { ContentForBody(textContent) } } } Row(modifier = modifier) { 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 2c92de4df9..f11e149073 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 @@ -618,7 +618,6 @@ private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready): Att type = AttachmentThumbnailType.Audio, ) is VoiceMessageType -> AttachmentThumbnailInfo( - textContent = messageContent.body, type = AttachmentThumbnailType.Voice, ) else -> null @@ -630,6 +629,7 @@ private fun textForInReplyTo(inReplyTo: InReplyTo.Ready): String { val messageContent = inReplyTo.content as? MessageContent ?: return "" return when (messageContent.type) { is LocationMessageType -> stringResource(CommonStrings.common_shared_location) + is VoiceMessageType -> stringResource(CommonStrings.common_voice_message) else -> messageContent.body } } diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png index 0ed5c052a6..975fb15bc4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-1_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1f8750e66aa2a60198fe68d2e3185bcf5a8708df81502f33c394d72f7b20a30 -size 42765 +oid sha256:e8e50be96a1b204b8187fd76802650569a740a927739135a6b6307481f2d5568 +size 40453 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png index 8ee1838413..872af5733e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-1_2_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbb1233cf288e6ffc1aaa7136b56c412699897d7edd20bef82784562e302bf40 -size 41215 +oid sha256:83f6cbcdbc00c49aed762339769e0f639604286406c46b1336b7757e2aad8b41 +size 38641 From 8c1893634fafb5b4fc7b4e36fcffc1f89151efe0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 01:52:03 +0000 Subject: [PATCH 236/281] Update dependency org.robolectric:robolectric to v4.11 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41eba3a713..863f3248b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -134,7 +134,7 @@ test_orchestrator = "androidx.test:orchestrator:1.4.2" test_turbine = "app.cash.turbine:turbine:1.0.0" test_truth = "com.google.truth:truth:1.1.5" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.14" -test_robolectric = "org.robolectric:robolectric:4.10.3" +test_robolectric = "org.robolectric:robolectric:4.11" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } # Others From 8350382440dd76c7fee8ba6e1bc0e0c4f723c721 Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 30 Oct 2023 00:10:04 +0000 Subject: [PATCH 237/281] Sync Strings from Localazy --- .../main/res/values-zh-rTW/translations.xml | 19 +++++++++++++++++++ .../main/res/values-zh-rTW/translations.xml | 6 +++--- .../src/main/res/values-cs/translations.xml | 1 + .../src/main/res/values-ru/translations.xml | 1 + .../src/main/res/values-sk/translations.xml | 2 ++ .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-sk/translations.xml | 2 ++ .../src/main/res/values-cs/translations.xml | 1 - .../src/main/res/values-ru/translations.xml | 1 - .../src/main/res/values-sk/translations.xml | 5 ++++- .../main/res/values-zh-rTW/translations.xml | 1 + .../src/main/res/values/localazy.xml | 2 -- 12 files changed, 34 insertions(+), 9 deletions(-) diff --git a/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml index 9d0c11fcd8..d01b56268e 100644 --- a/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml @@ -1,4 +1,23 @@ + + "PIN 碼錯誤。您還有 %1$d 次機會" + + "生物辨識認證" + "生物辨識解鎖" + "忘記 PIN 碼?" + "變更 PIN 碼" + "允許生物辨識解鎖" + "移除 PIN 碼" + "您確定要移除 PIN 碼嗎?" + "移除 PIN 碼" + "允許 %1$s" + "選擇 PIN 碼" + "確認 PIN 碼" + "基於安全性的考量,您選的 PIN 碼無法使用" + "選擇一個不一樣的 PIN 碼" + "請輸入相同的 PIN 碼兩次" + "PIN 碼不一樣" + "您即將登出" "正在登出…" diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml index e482fcc9f1..1c3e524b2b 100644 --- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml @@ -3,7 +3,7 @@ "更改帳號提供者" "家伺服器位址" "輸入關鍵字或網域名稱。" - "搜尋公司、社群、私有伺服器" + "搜尋公司、社群、私有伺服器。" "尋找帳號提供者" "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %s" @@ -21,10 +21,10 @@ "歡迎回來!" "登入 %1$s" "更改帳號提供者" - "Matrix 是一個開放網路,為了安全、去中心化的通訊而生。" + "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。" "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %1$s" "您即將在 %1$s 建立帳號" - "Matrix 是一個開放網路,為了安全、去中心化的通訊而生。" + "Matrix 是一個開放網路,為了安全且去中心化的通訊而生。" "歡迎使用 %1$s!" diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index 3bd09a0b76..48ea820a14 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -38,6 +38,7 @@ "Přidat emoji" "Zobrazit méně" "Držte pro nahrávání" + "Všichni" "Nahrání média se nezdařilo, zkuste to prosím znovu." "Pouze zmínky a klíčová slova" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index 2ba01ab763..46ae80803f 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -38,6 +38,7 @@ "Добавить эмодзи" "Показать меньше" "Удерживайте для записи" + "Для всех" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." "Только упоминания и ключевые слова" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index 7b89350ff8..8800aa7a22 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -5,6 +5,7 @@ "%1$d zmeny miestnosti" "%1$d zmien miestnosti" + "Informovať celú miestnosť" "Kamera" "Odfotiť" "Nahrať video" @@ -39,6 +40,7 @@ "Pridať emoji" "Zobraziť menej" "Podržaním nahrajte" + "Všetci" "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." "Iba zmienky a kľúčové slová" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 12b3ec04a1..17f791e752 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -5,7 +5,6 @@ "%1$d room changes" "Notify the whole room" - "Everyone" "Camera" "Take photo" "Record video" @@ -40,6 +39,7 @@ "Add emoji" "Show less" "Hold to record" + "Everyone" "Failed processing media to upload, please try again." "Mentions and Keywords only" diff --git a/features/roomlist/impl/src/main/res/values-sk/translations.xml b/features/roomlist/impl/src/main/res/values-sk/translations.xml index 0a1879a484..b4a8e8690a 100644 --- a/features/roomlist/impl/src/main/res/values-sk/translations.xml +++ b/features/roomlist/impl/src/main/res/values-sk/translations.xml @@ -1,5 +1,7 @@ + "Vaša záloha konverzácie nie je momentálne synchronizovaná. Na zachovanie prístupu k zálohe konverzácie musíte potvrdiť svoj kľúč na obnovu." + "Potvrďte svoj kľúč na obnovenie" "Vytvorte novú konverzáciu alebo miestnosť" "Začnite tým, že niekomu pošlete správu." "Zatiaľ žiadne konverzácie." 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 689f88cdee..e5f46af0e6 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -245,7 +245,6 @@ Pokud budete pokračovat, některá nastavení se mohou změnit." "en" "Chyba" "Úspěch" - "Všichni" "Sdílejte anonymní údaje o používání, které nám pomohou identifikovat problémy." "Můžete si přečíst všechny naše podmínky %1$s." "zde" 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 db0c685397..64d233e598 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -245,7 +245,6 @@ "en" "Ошибка" "Успешно" - "Для всех" "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы." "Вы можете ознакомиться со всеми нашими условиями %1$s." "здесь" 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 b24831294a..1a02692121 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -11,6 +11,7 @@ "Ukončená anketa" "Odoslať súbory" "Zobraziť heslo" + "Začať hovor" "Používateľské menu" "Nahrávanie hlasovej správy. Nahrávanie spustíte dvojitým ťuknutím a podržaním. Uvoľnením nahrávanie ukončíte." "Prijať" @@ -34,6 +35,7 @@ "Upraviť" "Povoliť" "Ukončiť anketu" + "Zadajte PIN" "Zabudnuté heslo?" "Preposlať" "Pozvať" @@ -134,6 +136,7 @@ "Nahlásiť chybu" "Nahlásenie bolo odoslané" "Rozšírený textový editor" + "Miestnosť" "Názov miestnosti" "napr. názov vášho projektu" "Zámok obrazovky" @@ -145,6 +148,7 @@ "URL adresa servera" "Nastavenia" "Zdieľaná poloha" + "Odhlasovanie" "Spustenie konverzácie…" "Nálepka" "Úspech" @@ -262,7 +266,6 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""sk" "Chyba" "Úspech" - "Všetci" "Zdieľajte anonymné údaje o používaní, aby sme mohli identifikovať problémy." "Môžete si prečítať všetky naše podmienky %1$s." "tu" 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 a87c7b50f3..37b372f46b 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 @@ -123,6 +123,7 @@ "格式化文字編輯器" "聊天室名稱" "範例:您的計畫名稱" + "螢幕鎖定" "搜尋結果" "安全性" "傳送中…" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 8ff9fc54b9..33a0789240 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -218,7 +218,6 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" - "Notify the whole room" "Share analytics data" "Failed selecting media, please try again." "Failed processing media to upload, please try again." @@ -263,7 +262,6 @@ If you proceed, some of your settings may change." "en" "Error" "Success" - "Everyone" "Share anonymous usage data to help us identify issues." "You can read all our terms %1$s." "here" From 2aa9bf967b93433854883c99d350aa2b680915a7 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Mon, 30 Oct 2023 11:41:15 +0100 Subject: [PATCH 238/281] Add a11y label to voice messages (#1683) --- .../timeline/components/event/TimelineItemVoiceView.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index e02773131d..67dee6be3b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -34,6 +34,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -64,8 +66,11 @@ fun TimelineItemVoiceView( state.eventSink(VoiceMessageEvents.PlayPause) } + val a11y = stringResource(CommonStrings.common_voice_message) Row( - modifier = modifier, + modifier = modifier.semantics { + contentDescription = a11y + }, verticalAlignment = Alignment.CenterVertically, ) { when (state.button) { From 7651cf1b33285b8c5ff1e29ca5b6f460f723e32b Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Mon, 30 Oct 2023 11:51:22 +0100 Subject: [PATCH 239/281] Voice message button: Show proper disabled color. (#1682) Also changes a bit our own IconButton api to allow to mirror material's and allow color customization. --- .../components/event/TimelineItemVoiceView.kt | 115 ++++++++---------- .../theme/components/IconButton.kt | 9 +- .../components/VoiceMessagePreview.kt | 11 +- ...ceView-D-41_41_null_14,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-41_41_null_4,NEXUS_5,1.0,en].png | 4 +- ...iceView-D-41_41_null_9,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_0,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_1,NEXUS_5,1.0,en].png | 4 +- ...ceView-N-41_42_null_10,NEXUS_5,1.0,en].png | 4 +- ...ceView-N-41_42_null_11,NEXUS_5,1.0,en].png | 4 +- ...ceView-N-41_42_null_12,NEXUS_5,1.0,en].png | 4 +- ...ceView-N-41_42_null_13,NEXUS_5,1.0,en].png | 4 +- ...ceView-N-41_42_null_14,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_2,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_3,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_4,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_5,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_6,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_7,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_8,NEXUS_5,1.0,en].png | 4 +- ...iceView-N-41_42_null_9,NEXUS_5,1.0,en].png | 4 +- ...ewUnified-D-42_42_null,NEXUS_5,1.0,en].png | 4 +- ...ewUnified-N-42_43_null,NEXUS_5,1.0,en].png | 4 +- ...elineView-N-8_9_null_8,NEXUS_5,1.0,en].png | 4 +- ...gePreview-D-16_16_null,NEXUS_5,1.0,en].png | 4 +- ...gePreview-N-16_17_null,NEXUS_5,1.0,en].png | 4 +- ...mposerVoice-D-4_4_null,NEXUS_5,1.0,en].png | 4 +- ...mposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 +- 28 files changed, 112 insertions(+), 123 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index 67dee6be3b..0c98321047 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -16,10 +16,7 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.annotation.DrawableRes import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -28,11 +25,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -51,6 +47,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.Text import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -78,7 +75,7 @@ fun TimelineItemVoiceView( VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause) VoiceMessageState.Button.Downloading -> ProgressButton() VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause) - VoiceMessageState.Button.Disabled -> DisabledPlayButton() + VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false) } Spacer(Modifier.width(8.dp)) Text( @@ -104,40 +101,54 @@ fun TimelineItemVoiceView( @Composable private fun PlayButton( - onClick: (() -> Unit) + onClick: () -> Unit, + enabled: Boolean = true, ) { - IconButton( - drawableRes = R.drawable.play, - contentDescription = stringResource(id = CommonStrings.a11y_play), - onClick = onClick - ) + CustomIconButton( + onClick = onClick, + enabled = enabled, + ) { + Icon( + resourceId = R.drawable.play, + contentDescription = stringResource(id = CommonStrings.a11y_play), + ) + } } @Composable private fun PauseButton( - onClick: (() -> Unit) + onClick: () -> Unit, ) { - IconButton( - drawableRes = R.drawable.pause, - contentDescription = stringResource(id = CommonStrings.a11y_play), - onClick = onClick - ) + CustomIconButton( + onClick = onClick, + ) { + Icon( + resourceId = R.drawable.pause, + contentDescription = stringResource(id = CommonStrings.a11y_play), + ) + } } @Composable private fun RetryButton( - onClick: (() -> Unit) + onClick: () -> Unit, ) { - IconButton( - drawableRes = R.drawable.retry, - contentDescription = stringResource(id = CommonStrings.action_retry), - onClick = onClick - ) + CustomIconButton( + onClick = onClick, + ) { + Icon( + resourceId = R.drawable.retry, + contentDescription = stringResource(id = CommonStrings.action_retry), + ) + } } @Composable private fun ProgressButton() { - Button { + CustomIconButton( + onClick = {}, + enabled = false, + ) { CircularProgressIndicator( modifier = Modifier .padding(2.dp) @@ -149,49 +160,23 @@ private fun ProgressButton() { } @Composable -private fun DisabledPlayButton() { - IconButton( - drawableRes = R.drawable.play, - contentDescription = null, - onClick = null, - ) -} - -@Composable -private fun IconButton( - @DrawableRes drawableRes: Int, - contentDescription: String?, - onClick: (() -> Unit)?, -) { - Button( - onClick = onClick, - ) { - Icon( - painter = painterResource(id = drawableRes), - contentDescription = contentDescription, - tint = ElementTheme.colors.iconSecondary, - modifier = Modifier.size(24.dp), - ) - } -} - -@Composable -private fun Button( - onClick: (() -> Unit)? = null, +private fun CustomIconButton( + onClick: () -> Unit, + enabled: Boolean = true, content: @Composable () -> Unit, ) { - Box( + IconButton( + onClick = onClick, modifier = Modifier - .size(36.dp) - .clip(CircleShape) - .background(ElementTheme.materialColors.background) - .let { - if (onClick != null) it.clickable(onClick = onClick) else it - }, - contentAlignment = Alignment.Center, - ) { - content() - } + .background(color = ElementTheme.colors.bgCanvasDefault, shape = CircleShape) + .size(36.dp), + enabled = enabled, + colors = IconButtonDefaults.iconButtonColors( + contentColor = ElementTheme.colors.iconSecondary, + disabledContentColor = ElementTheme.colors.iconDisabled, + ), + content = content, + ) } open class TimelineItemVoiceViewParametersProvider : PreviewParameterProvider { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt index 95132b11e2..fd6d2ed471 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/IconButton.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.material3.IconButtonColors import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable @@ -38,13 +39,13 @@ fun IconButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + colors: IconButtonColors = IconButtonDefaults.iconButtonColors( + contentColor = LocalContentColor.current, + disabledContentColor = ElementTheme.colors.iconDisabled, + ), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, content: @Composable () -> Unit ) { - val colors = IconButtonDefaults.iconButtonColors( - contentColor = LocalContentColor.current, - disabledContentColor = ElementTheme.colors.iconDisabled, - ) androidx.compose.material3.IconButton( onClick = onClick, modifier = modifier, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 5a4c1bd49e..97457b519d 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -114,10 +115,14 @@ private fun PlayerButton( ) { IconButton( onClick = onClick, - enabled = enabled, modifier = modifier .background(color = ElementTheme.colors.bgCanvasDefault, shape = CircleShape) - .size(30.dp.applyScaleUp()) + .size(30.dp.applyScaleUp()), + enabled = enabled, + colors = IconButtonDefaults.iconButtonColors( + contentColor = ElementTheme.colors.iconSecondary, + disabledContentColor = ElementTheme.colors.iconDisabled, + ), ) { when (type) { PlayerButtonType.Play -> PlayIcon() @@ -130,14 +135,12 @@ private fun PlayerButton( private fun PauseIcon() = Icon( resourceId = R.drawable.ic_pause, contentDescription = stringResource(id = CommonStrings.a11y_pause), - tint = ElementTheme.colors.iconSecondary, ) @Composable private fun PlayIcon() = Icon( resourceId = R.drawable.ic_play, contentDescription = stringResource(id = CommonStrings.a11y_play), - tint = ElementTheme.colors.iconSecondary, ) @PreviewsDayNight diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_14,NEXUS_5,1.0,en].png index 54d1cfa607..5bf6d017a8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcb3c53fff0cf52bf4ff8f8ca0bafaa7f21d1c7a6d504070de3de8f42dac2864 -size 9906 +oid sha256:ab61d047110a59bfbf327980f08572d9f6a48854a27e9ac256956ac465733043 +size 9884 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_4,NEXUS_5,1.0,en].png index b42ebe8d13..73b1551706 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f52f22ad35d11d5124cfcc5128f97e121021dd09caa19c96f23ae8f39aa8cfe -size 6053 +oid sha256:49066b0adf53941af950614f1090de66e372f1550d3f0a2b8f88d1bf255f7e3b +size 6055 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_9,NEXUS_5,1.0,en].png index 1a1c09685c..e3ec028872 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-D-41_41_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7eb5e7f99047da383a6bc5a45eb76bd902c21fcede196626fc4f6f5507097d51 -size 7333 +oid sha256:098a61f028e90cdf8b7e708793ebfaab20cde82a23ea117acb901469947dc713 +size 7335 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png index 223f192095..bf235fcdc6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bfffebea253932b08a308ac098a392910233e4a8524a898b459842849854c91 -size 6036 +oid sha256:9863144e5ee8fc010351509a11eac8d66332df97e56a8c5f81f2d0eb128fa368 +size 6806 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png index b6cab390c3..6e82a97ee0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b846d71d9d0019f55ee34e4ef38375f1501663a855f2c05742239a08d5b7501d -size 6020 +oid sha256:7e19c2813f5db4f4732d158e41211c2de175f9ab97c83296f719b5bfab615be2 +size 6775 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png index 1da72edd97..54510da6c4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca13f10ca5fd6194829c3cef02fa5cd0c00790f23c82d2cf70ac5d1d58babb54 -size 9656 +oid sha256:a294dfa6003a4bba8a780169ced0df7b8e9bb5bd4113dd21d2a5467325f58023 +size 10396 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png index 424216fd0a..903afbbde7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1dfdaca0c643c81a281658b4cee4d0e938d542a57964c3831496d04db543da45 -size 9827 +oid sha256:ba5a558037ae529515716a21b37070e5e04a9c0f6e0293a65a69cc3017a74b0d +size 10591 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png index aec364a0e6..bf6ad46dc4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad2ccde01384563471412843a48180b7acb3ba9bbedb9af60f96d403e564073d -size 9566 +oid sha256:28d5c962fb8528a96758e704d392a8444b1920dc36e49fd1bc684e5def2cf656 +size 10312 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png index cb45fe4553..32d7397228 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e32db0169cf50c2120a16efea3cb2b0f8a72f81ddce7a303c13bc2bf38c487ff -size 9653 +oid sha256:8adb090451574a5019a1e5c599fa6b2099035c830cc21cec816787e22fe4e690 +size 10329 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png index 5288039037..8dbfba27e0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:796854c3af1c45268c810f393842287d5fb3424f267f987f47bcd105e2a88448 -size 9621 +oid sha256:b9fd731cc3718c850372e3106969ec99180113dca0e41e02e5133b46a4efa745 +size 10319 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png index df1c6e821a..489b5572b6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0a6b88a5d7f001309dcb03da6151ac2949705b32a19301660a24fe02dc6fcdd -size 5562 +oid sha256:72875fc0e93851a621ba9ded610c736133188e3609a9578209a90bce9e674641 +size 6333 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png index 9a70f58bb9..c258cbcb9a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f8562e1e599e16ecc05ec0471de70b9de9a88b347f622bc723f21cb5379b102 -size 6033 +oid sha256:393205bc33da82255be0f2f19846bdda87baa5f97ca7c623681ad9537fa3e677 +size 6733 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png index 64a01a4889..d240797215 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32eba449e245eed3bc67c1ffdf47e267a6de2bf6832739f2902949af297391aa -size 6001 +oid sha256:9ecc36796f0404123f5ec125b4631535e50e1ad6ad70f5d933ae43879e580121 +size 6740 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png index aa9de6ea89..75742f88af 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4a2824fdc49adb0e3fc155e7da319414544a87dc083bdece78e85ba72669d78 -size 7155 +oid sha256:7e63f75edead31ff90194fedcdd6122503705d758959961466e652489f0819b0 +size 7879 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png index 4e5b8e146d..41c4a39ab9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3817e348ba2775528e3805ad6f5eed044f82b241f73692180c0638c95fb79fa2 -size 7076 +oid sha256:636a174a411d9d76212d1fe51717b147a2d0586ab55a73a29c42c3d139617f6e +size 7800 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png index 32b8d33a13..be1c277966 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd21f1c5ad2616b698a386d78d5efe0ab40aa1959382ed2a568c10441d093a8f -size 6692 +oid sha256:172b277c375702a57bdb204a0addadec0baa86236eab9130eb44b125bd615058 +size 7445 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png index dbee328561..73cf00afee 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07f52161378dfab92ffb76b860680390ccb5dfcc05347c5fd99504c806dd3210 -size 6930 +oid sha256:3eeb21741d4caccb07cc93db32887ec423d1c149a5b769bc3cd4cd0d88aa566d +size 7624 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png index d997afeeb6..6a67feef69 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4adce5249b20bdc9ed6f4d5f066140e8c9b0f62d04ebf953704a106a6ca2ff53 -size 7178 +oid sha256:4f3eb73f4a4ea4fb2e7b21d88393fab914b51cc63d9e01cd67fd4879ad856afe +size 7899 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-42_42_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-42_42_null,NEXUS_5,1.0,en].png index 6b0bdb01df..edf4815074 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-42_42_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-D-42_42_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c52825ef36b00ee13496c23db981c226a3f71c0d1755508835b4ab3a140cff6 -size 46402 +oid sha256:327a1fd51b242e1547878e58f32e6511e013cb440f4cd457a20e179ca3a1ed39 +size 46377 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png index d76cf9b40d..21c8e08e03 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ceb0cfa1b7ac95de658e339d29b4e18d8e4673d1a269db3d46bc6fd97381e9c2 -size 45460 +oid sha256:d277630d19b10150d18a660c06d38c02ce19cd5079667bd58cca385a7b540ae2 +size 54302 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png index c608f99a68..08ec54b681 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be817a2061d43e210c105b193f1baed6060c51f11d97f45d0593cb40ab7acd98 -size 56403 +oid sha256:e3be541cea4b8775aa141fd0f5e8d9b83f295684b4e9c8a070e9b1a4511e1531 +size 55884 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png index a7c99817a0..b2846a78f9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a53da79f9919d7f55884273fdcc473443a2d2a8eac36bc1e7c79baecff40b69e -size 22526 +oid sha256:8521c3615973ebf8b5d09a596a6122866648f45fa2015b3261dcb64505ebc41d +size 22459 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png index 479748f1f6..b4733b4baa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:195cb0e133075a7c8d2b429c32337dd62a7ce97ede7c0a62b630f03eaa145f63 -size 19293 +oid sha256:fca03bd3111ed7ee360ef60211c790aac9bb21dc3dcac8d0bcb8f3f315d6dd93 +size 19232 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png index 433aeb1bca..ba8791f9d8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2fe52c3e34e58ab38f7fa0b4b2281bdd8ef269acb3426449faa93fe7b434feb -size 27393 +oid sha256:4008401117a064b6eed9cf06ada8c15273a52dd729ae0fe989d075808c928cb5 +size 27376 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index 7bd842a1c4..7a466108e4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffa7daeaf5e27170395307c87c68a7674fb115bb9806cfc0d438a905571ce37f -size 25100 +oid sha256:875a8c7f394a0cd4c7944bf5cbf969a5e54108b8c3edee40c193d705c9ba3719 +size 25065 From e64f822f5a5ccb11eb357c2465b76a85e252c45d Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 30 Oct 2023 12:33:22 +0100 Subject: [PATCH 240/281] Enable Element Call integration in rooms by default (#1685) Also change base url to `call.element.dev`. --- .../io/element/android/appconfig/ElementCallConfig.kt | 2 +- changelog.d/+enable-element-call.feature | 1 + .../impl/advanced/AdvancedSettingsPresenter.kt | 2 +- .../impl/advanced/AdvancedSettingsPresenterTest.kt | 9 ++++----- .../android/libraries/featureflag/api/FeatureFlags.kt | 2 +- .../featureflag/impl/StaticFeatureFlagProvider.kt | 2 +- .../libraries/featureflag/test/FakeFeatureFlagService.kt | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 changelog.d/+enable-element-call.feature diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt index bbd9f62689..4182c32a42 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt @@ -17,5 +17,5 @@ package io.element.android.appconfig object ElementCallConfig { - const val DEFAULT_BASE_URL = "https://call.element.io" + const val DEFAULT_BASE_URL = "https://call.element.dev" } diff --git a/changelog.d/+enable-element-call.feature b/changelog.d/+enable-element-call.feature new file mode 100644 index 0000000000..6bee5170b2 --- /dev/null +++ b/changelog.d/+enable-element-call.feature @@ -0,0 +1 @@ +Enable Element Call integration in rooms by default. diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index 6359b34d0f..4bb5abfa19 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -82,7 +82,7 @@ class AdvancedSettingsPresenter @Inject constructor( validator = ::customElementCallUrlValidator, ) } else null, - eventSink = ::handleEvents + eventSink = { handleEvents(it) } ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index 11c79657ce..ec49d57fb9 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -44,6 +44,7 @@ class AdvancedSettingsPresenterTest { val initialState = awaitItem() assertThat(initialState.isDeveloperModeEnabled).isFalse() assertThat(initialState.isRichTextEditorEnabled).isFalse() + assertThat(initialState.customElementCallBaseUrlState).isNull() } } @@ -99,9 +100,7 @@ class AdvancedSettingsPresenterTest { @Test fun `present - custom element call base url`() = runTest { val store = InMemoryPreferencesStore() - val featureFlagService = FakeFeatureFlagService().apply { - setFeatureEnabled(FeatureFlags.InRoomCalls, true) - } + val featureFlagService = FakeFeatureFlagService(initialState = hashMapOf(FeatureFlags.InRoomCalls.key to true)) val presenter = AdvancedSettingsPresenter(store, featureFlagService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -113,9 +112,9 @@ class AdvancedSettingsPresenterTest { assertThat(initialState.customElementCallBaseUrlState).isNotNull() assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull() - initialState.eventSink(AdvancedSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.dev")) + initialState.eventSink(AdvancedSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy")) val updatedItem = awaitItem() - assertThat(updatedItem.customElementCallBaseUrlState?.baseUrl).isEqualTo("https://call.element.dev") + assertThat(updatedItem.customElementCallBaseUrlState?.baseUrl).isEqualTo("https://call.element.ahoy") } } 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 22daf73d69..f7dad97490 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 @@ -59,7 +59,7 @@ enum class FeatureFlags( key = "feature.elementcall", title = "Element call in rooms", description = "Allow user to start or join a call in a room", - defaultValue = false, + defaultValue = true, ), Mentions( key = "feature.mentions", 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 27ce0911d4..82471c5983 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 @@ -37,7 +37,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.NotificationSettings -> true FeatureFlags.VoiceMessages -> true FeatureFlags.PinUnlock -> false - FeatureFlags.InRoomCalls -> false + FeatureFlags.InRoomCalls -> true FeatureFlags.Mentions -> false } } else { diff --git a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt index 548ffa7cc4..9c86bad752 100644 --- a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt +++ b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt @@ -31,6 +31,6 @@ class FakeFeatureFlagService( } override suspend fun isFeatureEnabled(feature: Feature): Boolean { - return enabledFeatures[feature.key] ?: feature.defaultValue + return enabledFeatures[feature.key] ?: false } } From f736152945fde2a8d06b883bd6d7f1837f0d1d5f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 30 Oct 2023 12:41:16 +0100 Subject: [PATCH 241/281] Fix Maestro test --- .maestro/tests/account/logout.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/tests/account/logout.yaml b/.maestro/tests/account/logout.yaml index b0e8eda762..c2f75e0977 100644 --- a/.maestro/tests/account/logout.yaml +++ b/.maestro/tests/account/logout.yaml @@ -10,5 +10,5 @@ appId: ${APP_ID} # Ensure cancel cancels - tapOn: "Cancel" - tapOn: "Sign out" -- tapOn: "Sign out" +- tapOn: "Sign out anyway" - runFlow: ../assertions/assertInitDisplayed.yaml From 18ca1da1b55a4e96e049d60884e1ec5b7355235b Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 30 Oct 2023 12:01:39 +0000 Subject: [PATCH 242/281] Add haptic feedback to voice message record button (#1686) --- .../textcomposer/components/RecordButton.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt index 7c70dd1ef6..f8dd6dc05f 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt @@ -21,8 +21,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.preview.ElementPreview @@ -47,12 +49,26 @@ internal fun RecordButton( ) { val coroutineScope = rememberCoroutineScope() val pressState = rememberPressState() + val hapticFeedback = LocalHapticFeedback.current + + val performHapticFeedback = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } PressStateEffects( pressState = pressState.value, - onPressStart = onPressStart, - onLongPressEnd = onLongPressEnd, - onTap = onTap, + onPressStart = { + onPressStart() + performHapticFeedback() + }, + onLongPressEnd = { + onLongPressEnd() + performHapticFeedback() + }, + onTap = { + onTap() + performHapticFeedback() + }, ) RecordButtonView( From e98ed3f0aa950aa718d0b8ec8878af10fa383fef Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 30 Oct 2023 12:37:10 +0000 Subject: [PATCH 243/281] Keep screen on during voice message recording (#1684) --- .../features/messages/impl/MessagesView.kt | 3 +++ .../composer/VoiceMessageComposerPresenter.kt | 2 ++ .../composer/VoiceMessageComposerState.kt | 1 + .../VoiceMessageComposerStateProvider.kt | 2 ++ .../VoiceMessageComposerPresenterTest.kt | 24 +++++++++++++++++++ 5 files changed, 32 insertions(+) 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 5c08c62849..cc89ba3658 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 @@ -93,6 +93,7 @@ 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.designsystem.utils.CommonDrawables +import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost @@ -124,6 +125,8 @@ fun MessagesView( state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event)) } + KeepScreenOn(state.voiceMessageComposerState.keepScreenOn) + AttachmentStateView( state = state.composerState.attachmentsState, onPreviewAttachments = onPreviewAttachments, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index c8edec15a6..7cde5739ba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -64,6 +64,7 @@ class VoiceMessageComposerPresenter @Inject constructor( override fun present(): VoiceMessageComposerState { val localCoroutineScope = rememberCoroutineScope() val recorderState by voiceRecorder.state.collectAsState(initial = VoiceRecorderState.Idle) + val keepScreenOn by remember { derivedStateOf { recorderState is VoiceRecorderState.Recording } } val permissionState = permissionsPresenter.present() var isSending by remember { mutableStateOf(false) } @@ -191,6 +192,7 @@ class VoiceMessageComposerPresenter @Inject constructor( else -> VoiceMessageState.Idle }, showPermissionRationaleDialog = permissionState.showDialog, + keepScreenOn = keepScreenOn, eventSink = handleEvents, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt index fc7f0ad15f..055fa28177 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerState.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState data class VoiceMessageComposerState( val voiceMessageState: VoiceMessageState, val showPermissionRationaleDialog: Boolean, + val keepScreenOn: Boolean, val eventSink: (VoiceMessageComposerEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt index 150381461b..f9856005bb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerStateProvider.kt @@ -30,10 +30,12 @@ internal open class VoiceMessageComposerStateProvider : PreviewParameterProvider internal fun aVoiceMessageComposerState( voiceMessageState: VoiceMessageState = VoiceMessageState.Idle, + keepScreenOn: Boolean = false, showPermissionRationaleDialog: Boolean = false, ) = VoiceMessageComposerState( voiceMessageState = voiceMessageState, showPermissionRationaleDialog = showPermissionRationaleDialog, + keepScreenOn = keepScreenOn, eventSink = {}, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index c0d1723c36..b35f5c66b3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -101,6 +101,30 @@ class VoiceMessageComposerPresenterTest { } } + @Test + fun `present - recording keeps screen on`() = runTest { + val presenter = createVoiceMessageComposerPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().apply { + eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart)) + assertThat(keepScreenOn).isFalse() + } + + awaitItem().apply { + assertThat(keepScreenOn).isTrue() + eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd)) + } + + val finalState = awaitItem().apply { + assertThat(keepScreenOn).isFalse() + } + + testPauseAndDestroy(finalState) + } + } + @Test fun `present - abort recording`() = runTest { val presenter = createVoiceMessageComposerPresenter() From fa66ffc06dccf3f895a3f13f3121a29ebe73865f Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 30 Oct 2023 12:41:05 +0000 Subject: [PATCH 244/281] Improve composer UI scaling (#1689) --- .../element/android/libraries/textcomposer/TextComposer.kt | 6 +++--- .../textcomposer/components/ComposerOptionsButton.kt | 2 +- .../textcomposer/components/DismissTextFormattingButton.kt | 2 +- .../libraries/textcomposer/components/FormattingOption.kt | 6 ++++-- .../libraries/textcomposer/components/LiveWaveformView.kt | 3 ++- .../libraries/textcomposer/components/RecordButton.kt | 2 +- .../textcomposer/components/VoiceMessageDeleteButton.kt | 2 +- .../textcomposer/components/VoiceMessagePreview.kt | 4 +++- .../textcomposer/components/VoiceMessageRecording.kt | 7 ++++--- 9 files changed, 20 insertions(+), 14 deletions(-) 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 a9236c6bfd..513b5f6a3f 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 @@ -136,7 +136,7 @@ fun TextComposer( @Composable { ComposerOptionsButton( modifier = Modifier - .size(48.dp), + .size(48.dp.applyScaleUp()), onClick = onAddAttachment ) } @@ -183,7 +183,7 @@ fun TextComposer( } val uploadVoiceProgress = @Composable { CircularProgressIndicator( - modifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp.applyScaleUp()), ) } @@ -429,7 +429,7 @@ private fun TextInput( // This prevents it gaining focus and mutating the state. registerStateUpdates = !subcomposing, modifier = Modifier - .padding(top = 6.dp, bottom = 6.dp) + .padding(top = 6.dp.applyScaleUp(), bottom = 6.dp.applyScaleUp()) .fillMaxWidth(), style = ElementRichTextEditorStyle.create( hasFocus = state.hasFocus diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt index d1c7355861..6721a9bf4c 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/ComposerOptionsButton.kt @@ -37,7 +37,7 @@ internal fun ComposerOptionsButton( ) { IconButton( modifier = modifier - .size(48.dp), + .size(48.dp.applyScaleUp()), onClick = onClick ) { Icon( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt index c6ebe270bf..51a5f1b09e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/DismissTextFormattingButton.kt @@ -37,7 +37,7 @@ internal fun DismissTextFormattingButton( ) { IconButton( modifier = modifier - .size(48.dp), + .size(48.dp.applyScaleUp()), onClick = onClick ) { Icon( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt index b7874af386..eed1e490f7 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt @@ -68,7 +68,7 @@ internal fun FormattingOption( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple( bounded = false, - radius = 20.dp, + radius = 20.dp.applyScaleUp(), ), ) .size(48.dp.applyScaleUp()) @@ -80,7 +80,9 @@ internal fun FormattingOption( .background(backgroundColor, shape = RoundedCornerShape(8.dp.applyScaleUp())) ) { Icon( - modifier = Modifier.align(Alignment.Center), + modifier = Modifier + .align(Alignment.Center) + .size(20.dp.applyScaleUp()), imageVector = imageVector, contentDescription = contentDescription, tint = foregroundColor, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt index d92851c378..ae2544abf4 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/LiveWaveformView.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.media.drawWaveform import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -70,7 +71,7 @@ fun LiveWaveformView( Box(contentAlignment = Alignment.CenterEnd, modifier = modifier .fillMaxWidth() - .height(waveFormHeight) + .height(waveFormHeight.applyScaleUp()) .onSizeChanged { parentWidth = it.width } ) { Canvas( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt index f8dd6dc05f..58a674f8cf 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt @@ -97,7 +97,7 @@ private fun RecordButtonView( ) { IconButton( modifier = modifier - .size(48.dp), + .size(48.dp.applyScaleUp()), onClick = {}, ) { Icon( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt index 4d584d674e..87a837f405 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt @@ -39,7 +39,7 @@ fun VoiceMessageDeleteButton( ) { IconButton( modifier = modifier - .size(48.dp), + .size(48.dp.applyScaleUp()), enabled = enabled, onClick = onClick, ) { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 97457b519d..efd7bf72e1 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -66,7 +66,7 @@ internal fun VoiceMessagePreview( shape = MaterialTheme.shapes.medium, ) .padding(start = 8.dp, end = 20.dp, top = 6.dp, bottom = 6.dp) - .heightIn(26.dp), + .heightIn(26.dp.applyScaleUp()), verticalAlignment = Alignment.CenterVertically, ) { if (isPlaying) { @@ -135,12 +135,14 @@ private fun PlayerButton( private fun PauseIcon() = Icon( resourceId = R.drawable.ic_pause, contentDescription = stringResource(id = CommonStrings.a11y_pause), + modifier = Modifier.size(20.dp.applyScaleUp()), ) @Composable private fun PlayIcon() = Icon( resourceId = R.drawable.ic_play, contentDescription = stringResource(id = CommonStrings.a11y_play), + modifier = Modifier.size(20.dp.applyScaleUp()), ) @PreviewsDayNight diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt index 608a20200d..255ad6dc88 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecording.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.applyScaleUp import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.utils.time.formatShort @@ -62,7 +63,7 @@ internal fun VoiceMessageRecording( shape = MaterialTheme.shapes.medium, ) .padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) - .heightIn(26.dp), + .heightIn(26.dp.applyScaleUp()), verticalAlignment = Alignment.CenterVertically, ) { RedRecordingDot() @@ -80,7 +81,7 @@ internal fun VoiceMessageRecording( LiveWaveformView( modifier = Modifier - .height(26.dp) + .height(26.dp.applyScaleUp()) .weight(1f), levels = levels ) @@ -103,7 +104,7 @@ private fun RedRecordingDot( ) Box( modifier = modifier - .size(8.dp) + .size(8.dp.applyScaleUp()) .alpha(alpha) .background(color = ElementTheme.colors.textCriticalPrimary, shape = CircleShape) ) From ffd02b831ed00364d0c3eac30ec8549d2c5da7a8 Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 30 Oct 2023 14:50:36 +0000 Subject: [PATCH 245/281] Sync Strings from Localazy --- .../src/main/res/values-fr/translations.xml | 28 +++++++++++++ .../src/main/res/values-sk/translations.xml | 3 -- .../src/main/res/values-fr/translations.xml | 1 + .../src/main/res/values-sk/translations.xml | 2 - .../src/main/res/values-fr/translations.xml | 10 +++++ .../src/main/res/values-fr/translations.xml | 6 ++- .../src/main/res/values-fr/translations.xml | 3 ++ .../src/main/res/values-fr/translations.xml | 1 + .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-sk/translations.xml | 5 --- .../src/main/res/values-fr/translations.xml | 2 + .../src/main/res/values-fr/translations.xml | 42 +++++++++++++++++++ .../src/main/res/values-fr/translations.xml | 8 ++++ .../src/main/res/values-fr/translations.xml | 27 ++++++++++++ .../src/main/res/values-sk/translations.xml | 6 --- 15 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 features/securebackup/impl/src/main/res/values-fr/translations.xml create mode 100644 features/signedout/impl/src/main/res/values-fr/translations.xml diff --git a/features/lockscreen/impl/src/main/res/values-fr/translations.xml b/features/lockscreen/impl/src/main/res/values-fr/translations.xml index 64944ff843..5a56f1b6f2 100644 --- a/features/lockscreen/impl/src/main/res/values-fr/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-fr/translations.xml @@ -1,4 +1,32 @@ + + "Il reste %1$d tentative pour déverrouiller" + "Il reste %1$d tentatives pour déverrouiller" + + + "Code PIN incorrect. Il reste %1$d tentative" + "Code PIN incorrect. Il reste %1$d tentatives" + + "Authentification biométrique" + "Déverrouillage biométrique" + "Code PIN oublié?" + "Modifier le code PIN" + "Autoriser le déverrouillage biométrique" + "Supprimer le code PIN" + "Êtes-vous certain de vouloir supprimer le code PIN?" + "Supprimer le code PIN?" + "Autoriser %1$s" + "Je préfère utiliser le code PIN" + "Gagnez du temps en utilisant %1$s pour déverrouiller l’application à chaque fois." + "Choisissez un code PIN" + "Confirmer le code PIN" + "Vous ne pouvez pas choisir ce code PIN pour des raisons de sécurité" + "Choisissez un code PIN différent" + "Verrouillez %1$s pour ajouter une sécurité supplémentaire à vos discussions. Choisissez un code facile à retenir. Si vous oubliez le code PIN, vous serez déconnecté." + "Veuillez saisir le même code PIN deux fois" + "Les codes PIN ne correspondent pas" + "Pour continuer, vous devrez vous connecter à nouveau et créer un nouveau code PIN." + "Vous êtes en train de vous déconnecter" "Déconnexion…" diff --git a/features/lockscreen/impl/src/main/res/values-sk/translations.xml b/features/lockscreen/impl/src/main/res/values-sk/translations.xml index fd4d8f6da7..0977768f81 100644 --- a/features/lockscreen/impl/src/main/res/values-sk/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sk/translations.xml @@ -1,8 +1,5 @@ - - "Máte 3 pokusy na odomknutie" - "Nesprávny PIN kód. Máte ešte %1$d pokus" "Nesprávny PIN kód. Máte ešte %1$d pokusy" diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index 7362aac3e8..eedba864da 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -18,6 +18,7 @@ "URL du serveur d’accueil" "Vous ne pouvez vous connecter qu’à un serveur existant qui prend en charge le sliding sync. L’administrateur de votre serveur d’accueil devra le configurer. %1$s" "Quelle est l’adresse de votre serveur ?" + "Choisissez votre serveur" "Ce compte a été désactivé." "Nom d’utilisateur et/ou mot de passe incorrects" "Il ne s’agit pas d’un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »" diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml index ef7ba0d8dc..cf49504755 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -14,9 +14,7 @@ "Použite iného poskytovateľa účtu, ako napríklad vlastný súkromný server alebo pracovný účet." "Zmeniť poskytovateľa účtu" "Nemohli sme sa spojiť s týmto domovským serverom. Skontrolujte prosím, či ste zadali URL adresu domovského servera správne. Ak je adresa URL správna, kontaktujte svoj domovský server pre ďalšiu pomoc." - "Tento server momentálne nepodporuje kĺzavú synchronizáciu." "Adresa URL domovského servera" - "Pripojiť sa môžete len k existujúcemu serveru, ktorý podporuje kĺzavú synchronizáciu. Váš správca domovského servera ju bude musieť nakonfigurovať. %1$s" "Aká je adresa vášho servera?" "Vyberte svoj server" "Tento účet bol deaktivovaný." diff --git a/features/logout/impl/src/main/res/values-fr/translations.xml b/features/logout/impl/src/main/res/values-fr/translations.xml index 16c9d3717e..c309c3166f 100644 --- a/features/logout/impl/src/main/res/values-fr/translations.xml +++ b/features/logout/impl/src/main/res/values-fr/translations.xml @@ -3,6 +3,16 @@ "Êtes-vous sûr de vouloir vous déconnecter ?" "Se déconnecter" "Déconnexion…" + "Vous êtes en train de vous déconnecter de votre dernière session. Si vous vous déconnectez maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées." + "Vous avez désactivé la sauvegarde" + "Vos clés étaient en cours de sauvegarde lorsque vous avez perdu la connexion au réseau. Il faudrait rétablir cette connexion afin de pouvoir terminer la sauvegarde avant de vous déconnecter." + "Vos clés sont en cours de sauvegarde" + "Veuillez attendre que cela se termine avant de vous déconnecter." + "Vos clés sont en cours de sauvegarde" + "Vous êtes sur le point de vous déconnecter de votre dernier appareil. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos messages." + "La récupération n’est pas configurée." + "Vous êtes sur le point de vous déconnecter de votre dernière session. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées." + "Avez-vous sauvegardé votre clé de récupération?" "Se déconnecter" "Se déconnecter" diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index aed47e5ff8..f68a29ce16 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -4,6 +4,7 @@ "%1$d changement dans le salon" "%1$d changements dans le salon" + "Notifier tout le salon" "Appareil photo" "Prendre une photo" "Enregistrer une vidéo" @@ -13,8 +14,9 @@ "Sondage" "Formatage du texte" "L’historique des messages n’est actuellement pas disponible dans ce salon" + "L’historique de la discussion n’est pas disponible. Vérifiez cette session pour accéder à l’historique." "Impossible de récupérer les détails de l’utilisateur" - "Souaitez-vous inviter l\'ancien membre à revenir ?" + "Souhaitez-vous inviter l’ancien membre à revenir ?" "Vous êtes seul dans ce salon" "Message copié" "Vous n’êtes pas autorisé à publier dans ce salon" @@ -36,6 +38,8 @@ "Votre message n’a pas pu être envoyé" "Ajouter un émoji" "Afficher moins" + "Maintenir pour enregistrer" + "Tout le monde" "Échec du traitement des médias à télécharger, veuillez réessayer." "Mentions et mots clés uniquement" 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 3466ecd630..85ac80b5d9 100644 --- a/features/preferences/impl/src/main/res/values-fr/translations.xml +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,8 @@ + "URL de base pour Element Call personnalisée" + "Configurer une URL de base pour Element Call." + "URL invalide, assurez-vous d’inclure le protocol (http/https) et l’adresse correcte." "Mode développeur" "Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs." "Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown." diff --git a/features/rageshake/api/src/main/res/values-fr/translations.xml b/features/rageshake/api/src/main/res/values-fr/translations.xml index 2031490c1f..4b41fdc0cb 100644 --- a/features/rageshake/api/src/main/res/values-fr/translations.xml +++ b/features/rageshake/api/src/main/res/values-fr/translations.xml @@ -1,4 +1,5 @@ "%1$s s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?" + "Vous semblez secouez votre téléphone avec frustration. Souhaitez-vous ouvrir le formulaire pour reporter un problème?" 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 b17ef35e9b..b02fcbc8e8 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -1,7 +1,7 @@ - "1 osoba" + "%1$d osoba" "%1$d osoby" "%1$d osob" 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 5d6a4131b5..86f1724be7 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -1,10 +1,5 @@ - - "1 osoba" - "%1$d ľudia" - "%1$d ľudí" - "Pridať tému" "Už ste členom" "Už ste pozvaní" diff --git a/features/roomlist/impl/src/main/res/values-fr/translations.xml b/features/roomlist/impl/src/main/res/values-fr/translations.xml index 2a14d1f4f4..57ad02caca 100644 --- a/features/roomlist/impl/src/main/res/values-fr/translations.xml +++ b/features/roomlist/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,7 @@ + "La sauvegarde des conversations est désynchronisée. Vous devez confirmer la clé de récupération pour accéder à votre historique." + "Confirmer votre clé de récupération" "Créer une nouvelle discussion ou un nouveau salon" "Commencez par envoyer un message à quelqu’un." "Aucune discussion pour le moment." diff --git a/features/securebackup/impl/src/main/res/values-fr/translations.xml b/features/securebackup/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..3922f2fa78 --- /dev/null +++ b/features/securebackup/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,42 @@ + + + "Désactiver la sauvegarde" + "Activer la sauvegarde" + "La sauvegarde assure que vous ne perdiez pas l’historique des discussions. %1$s." + "Sauvegarde" + "Changer la clé de récupération" + "Confirmer la clé de récupération" + "La sauvegarde des discussions est désynchronisée." + "Configurer la récupération" + "Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnectés de %1$s partout." + "Désactiver" + "Vous perdrez vos messages chiffrés si vous vous déconnectez de toutes vos sessions." + "Êtes-vous certain de vouloir désactiver la sauvegarde?" + "Désactiver la sauvegarde supprimera votre clé de récupération actuelle et désactivera d’autres mesures de sécurité. Dans ce cas, vous:" + "Pas d’accès à l’historique des discussions chiffrées sur vos nouveaux appareils" + "Perte de l’accès à vos messages chiffrés si vous êtes déconnectés de %1$s partout" + "Êtes-vous certain de vouloir désactiver la sauvegarde?" + "Obtenez une nouvelle clé de récupération dans le cas où vous avez oublié l’ancienne. Après le changement, l’ancienne clé ne sera plus utilisable." + "Générer une nouvelle clé" + "Assurez-vous de conserver la clé dans un endroit sûr." + "Clé de récupération modifée" + "Changer la clé de récupération?" + "Saisissez votre clé de récupération pour accéder à l’historique de vos discussions." + "Saisissez la clé à 48 caractères." + "Saisissez la clé ici…" + "Clé de récupération confirmée" + "Confirmez votre clé de récupération" + "Clé de récupération copiée" + "Génération…" + "Enregistrer la clé" + "Recopier votre clé de récupération dans un endroit sécurisé ou enregistrer la dans un manager de mot de passe." + "Taper pour copier la clé" + "Sauvegarder la clé" + "La clé ne pourra plus être affichée après cette étape." + "Avez-vous sauvegardé votre clé de récupération?" + "Votre sauvegarde est protégée par votre clé de récupération. Si vous avez besoin d’une nouvelle clé après la configuration, vous pourrez en créer une nouvelle en cliquant sur \"Changer la clé de récupération\"" + "Générer la clé de récupération" + "Assurez-vous de pouvoir enregistrer votre clé dans un endroit sécurisé." + "Sauvegarde mise en place avec succès" + "Configurer la sauvegarde" + diff --git a/features/signedout/impl/src/main/res/values-fr/translations.xml b/features/signedout/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..8b4c9b7461 --- /dev/null +++ b/features/signedout/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,8 @@ + + + "Le mot de passe de votre compte a été modifié sur un autre appareil" + "Cette session a été supprimée depuis un autre appareil" + "L’administrateur de votre serveur a révoqué votre accès." + "La déconnexion peut être due à une des raisons ci-dessous. Veuillez vous connecter à nouveau pour continuer à utiliser %1$s." + "Vous avez été déconnecté" + 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 f0de166ff1..f655b1eafa 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -1,13 +1,19 @@ + "Supprimer" "Masquer le mot de passe" "Mentions uniquement" "En sourdine" + "Pause" + "Code PIN" + "Lecture" "Sondage" "Sondage terminé" "Envoyer des fichiers" "Afficher le mot de passe" + "Démarrer un appel" "Menu utilisateur" + "Enregistrer un message vocal. Taper deux fois et maintenir pour enregistrer. Relâcher pour stopper l’enregistrement." "Accepter" "Ajouter à la discussion" "Retour" @@ -29,6 +35,7 @@ "Modifier" "Activer" "Terminer le sondage" + "Saisir le code PIN" "Mot de passe oublié ?" "Transférer" "Inviter" @@ -64,12 +71,16 @@ "Envoyer un message" "Partager" "Partager le lien" + "Se connecter à nouveau" + "Se déconnecter" + "Se déconnecter quand même" "Passer" "Démarrer" "Démarrer une discussion" "Commencer la vérification" "Cliquez pour charger la carte" "Prendre une photo" + "Essayer à nouveau" "Afficher la source" "Oui" "Modifier le sondage" @@ -79,6 +90,7 @@ "Statistiques d’utilisation" "Audio" "Bulles" + "Sauvegarde des discussions" "Droits d’auteur" "Création du salon…" "Quitter le salon" @@ -88,7 +100,9 @@ "Édition" "* %1$s %2$s" "Chiffrement activé" + "Saisissez votre code PIN" "Erreur" + "Tout le monde" "Fichier" "Fichier enregistré dans Téléchargements" "Transférer le message" @@ -116,13 +130,16 @@ "Politique de confidentialité" "Réaction" "Réactions" + "Clé de récupération" "Actualisation…" "En réponse à %1$s" "Signaler un problème" "Rapport soumis" "Éditeur de texte enrichi" + "Salon" "Nom du salon" "par exemple, le nom de votre projet" + "Verrouillage d’écran" "Rechercher quelqu’un" "Résultats de la recherche" "Sécurité" @@ -131,6 +148,7 @@ "URL du serveur" "Paramètres" "Position partagée" + "Déconnexion" "Création de la discussion…" "Autocollant" "Succès" @@ -144,13 +162,16 @@ "Échec de déchiffrement" "Les invitations n’ont pas pu être envoyées à un ou plusieurs utilisateurs." "Impossible d’envoyer une ou plusieurs invitations" + "Déverrouillage" "Annuler la sourdine" "Événement non pris en charge" "Nom d’utilisateur" "Vérification annulée" "Vérification terminée" "Vidéo" + "Message vocal" "En attente…" + "En attente de la clé de déchiffrement" "Êtes-vous sûr de vouloir mettre fin à ce sondage ?" "Sondage : %1$s" "Confirmation" @@ -167,8 +188,10 @@ "%1$s n’a pas pu charger la carte. Veuillez réessayer ultérieurement." "Échec du chargement des messages" "%1$s n’a pas pu accéder à votre position. Veuillez réessayer ultérieurement." + "Échec lors de l’envoi du message vocal." "%1$s n’est pas autorisé à accéder à votre position. Vous pouvez activer l’accès dans les Paramètres." "%1$s n’est pas autorisé à accéder à votre position. Activez l’accès ci-dessous." + "%1$s n’a pas l’autorisation d’accéder au microphone. Autorisez l’accès pour enregistrer un message." "Certains messages n’ont pas été envoyés" "Désolé, une erreur s’est produite" "🔐️ Rejoignez-moi sur %1$s" @@ -177,6 +200,10 @@ "Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation." "Êtes-vous sûr de vouloir quitter le salon ?" "%1$s Android" + + "%1$d chiffre saisi" + "%1$d chiffres saisis" + "%1$d membre" "%1$d membres" 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 1a02692121..0f5d20df6b 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -210,11 +210,6 @@ "%1$d členovia" "%1$d členov" - - "1 hlas" - "%d hlasy" - "%d hlasov" - "Zúrivo potriasť pre nahlásenie chyby" "Táto správa bude nahlásená správcovi vášho domovského servera. Nebude môcť prečítať žiadne šifrované správy." "Dôvod nahlásenia tohto obsahu" @@ -256,7 +251,6 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""Zdieľať polohu" "Zdieľať moju polohu" "Otvoriť v Apple Maps" - "Otvoriť v Mapách Google" "Otvoriť v OpenStreetMap" "Zdieľajte túto polohu" "Poloha" From 356a32178866dc811f1edc384e6d13e92bbb8376 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 30 Oct 2023 17:01:54 +0100 Subject: [PATCH 246/281] Fix issues with stuck 'loading...' state and hangup (#1690) * Fix issues with stuck 'loading...' state and hangu --- .../features/call/data/WidgetMessage.kt | 6 +++- .../features/call/ui/CallScreenPresenter.kt | 30 +++++++++++++++---- .../features/call/ui/CallScreenView.kt | 6 +--- .../utils/WebViewWidgetMessageInterceptor.kt | 3 +- .../matrix/impl/widget/RustWidgetDriver.kt | 3 +- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt b/features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt index 11396c551f..2b38f6850b 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/data/WidgetMessage.kt @@ -18,6 +18,7 @@ package io.element.android.features.call.data import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement @Serializable data class WidgetMessage( @@ -25,6 +26,7 @@ data class WidgetMessage( @SerialName("widgetId") val widgetId: String, @SerialName("requestId") val requestId: String, @SerialName("action") val action: Action, + @SerialName("data") val data: JsonElement? = null, ) { @Serializable @@ -38,6 +40,8 @@ data class WidgetMessage( @Serializable enum class Action { @SerialName("im.vector.hangup") - HangUp + HangUp, + @SerialName("send_event") + SendEvent, } } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt index a29f43af80..f9bb8bde2f 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt @@ -19,9 +19,12 @@ package io.element.android.features.call.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +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 dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -41,6 +44,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import java.util.UUID class CallScreenPresenter @AssistedInject constructor( @@ -66,6 +72,7 @@ class CallScreenPresenter @AssistedInject constructor( val urlState = remember { mutableStateOf>(Async.Uninitialized) } val callWidgetDriver = remember { mutableStateOf(null) } val messageInterceptor = remember { mutableStateOf(null) } + var isJoinedCall by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { loadUrl(callType, urlState, callWidgetDriver) @@ -92,8 +99,16 @@ class CallScreenPresenter @AssistedInject constructor( callWidgetDriver.value?.send(it) val parsedMessage = parseMessage(it) - if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget && parsedMessage.action == WidgetMessage.Action.HangUp) { - close(callWidgetDriver.value, navigator) + if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) { + if (parsedMessage.action == WidgetMessage.Action.HangUp) { + close(callWidgetDriver.value, navigator) + } else if (parsedMessage.action == WidgetMessage.Action.SendEvent) { + // This event is received when a member joins the call, the first one will be the current one + val type = parsedMessage.data?.jsonObject?.get("type")?.jsonPrimitive?.contentOrNull + if (type == "org.matrix.msc3401.call.member") { + isJoinedCall = true + } + } } } .launchIn(this) @@ -105,11 +120,13 @@ class CallScreenPresenter @AssistedInject constructor( is CallScreeEvents.Hangup -> { val widgetId = callWidgetDriver.value?.id val interceptor = messageInterceptor.value - if (widgetId != null && interceptor != null) { + if (widgetId != null && interceptor != null && isJoinedCall) { + // If the call was joined, we need to hang up first. Then the UI will be dismissed automatically. sendHangupMessage(widgetId, interceptor) - } - coroutineScope.launch { - close(callWidgetDriver.value, navigator) + } else { + coroutineScope.launch { + close(callWidgetDriver.value, navigator) + } } } is CallScreeEvents.SetupMessageChannels -> { @@ -159,6 +176,7 @@ class CallScreenPresenter @AssistedInject constructor( widgetId = widgetId, requestId = "widgetapi-${clock.epochMillis()}", action = WidgetMessage.Action.HangUp, + data = null, ) messageInterceptor.sendMessage(WidgetMessageSerializer.serialize(message)) } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt index 9ac06a63b7..33611515a7 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt @@ -111,12 +111,8 @@ private fun CallWebView( modifier = modifier, factory = { context -> WebView(context).apply { - setup(userAgent, onPermissionsRequested) - if (url is Async.Success) { - loadUrl(url.data) - } - onWebViewCreated(this) + setup(userAgent, onPermissionsRequested) } }, update = { webView -> diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt index bdb6ee48f5..e11529f068 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/WebViewWidgetMessageInterceptor.kt @@ -36,7 +36,8 @@ class WebViewWidgetMessageInterceptor( const val LISTENER_NAME = "elementX" } - override val interceptedMessages = MutableSharedFlow(replay = 1, extraBufferCapacity = 2) + // It's important to have extra capacity here to make sure we don't drop any messages + override val interceptedMessages = MutableSharedFlow(extraBufferCapacity = 10) init { webView.webViewClient = object : WebViewClient() { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt index 33e829a206..641be4c618 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/RustWidgetDriver.kt @@ -36,7 +36,8 @@ class RustWidgetDriver( private val widgetCapabilitiesProvider: WidgetCapabilitiesProvider, ): MatrixWidgetDriver { - override val incomingMessages = MutableSharedFlow() + // It's important to have extra capacity here to make sure we don't drop any messages + override val incomingMessages = MutableSharedFlow(extraBufferCapacity = 10) private val driverAndHandle = makeWidgetDriver(widgetSettings.toRustWidgetSettings()) private var receiveMessageJob: Job? = null From 609b100a76846e65c89a2743704673bc7da67c55 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Mon, 30 Oct 2023 18:22:42 +0100 Subject: [PATCH 247/281] Fix wrong CompoundColors mapping (#1692) * Fix wrong CompoundColors mapping Fixes https://github.com/vector-im/compound/issues/262 * Update screenshots --------- Co-authored-by: ElementBot --- .../android/libraries/theme/compound/CompoundColors.kt | 4 ++-- ...S_t[appnav.root_null_Root-N-3_4_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...S_t[appnav.root_null_Root-N-3_4_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...S_t[appnav.root_null_Root-N-3_4_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...t_null_CreateRoomRootView-N-4_5_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...pl.welcome_null_WelcomeView-N-2_3_null,NEXUS_5,1.0,en].png | 4 ++-- ....impl_null_InviteListView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ....impl_null_InviteListView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ....impl_null_InviteListView-N-0_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ....impl_null_InviteListView-N-0_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...om.api_null_LeaveRoomView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...om.api_null_LeaveRoomView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...om.api_null_LeaveRoomView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...om.api_null_LeaveRoomView-N-0_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...end_null_SendLocationView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...end_null_SendLocationView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...how_null_ShowLocationView-N-1_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...how_null_ShowLocationView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ll_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...l.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...l.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...idingSyncNotSupportedDialog-N-2_3_null,NEXUS_5,1.0,en].png | 4 ++-- ...rd_null_LoginPasswordView-N-6_7_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...tscreen_null_WaitListView-N-8_9_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...w_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ..._null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...a.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...rt_null_ReportMessageView-N-7_8_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png | 4 ++-- ..._TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...l_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png | 4 ++-- ...imeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...s.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...reate_null_CreatePollView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ltNotificationSettingView-N-8_9_null_3,NEXUS_5,1.0,en].png | 4 ++-- ..._NotificationSettingsView-N-5_6_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ash_null_CrashDetectionView-N-0_1_null,NEXUS_5,1.0,en].png | 4 ++-- ...null_RageshakeDialogContent-N-1_2_null,NEXUS_5,1.0,en].png | 4 ++-- ..._null_RoomDetailsEditView-N-0_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ..._RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png | 4 ++-- ..._RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png | 4 ++-- ..._RoomNotificationSettings-N-4_5_null_3,NEXUS_5,1.0,en].png | 4 ++-- ..._RoomNotificationSettings-N-4_5_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ll_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ialogs_null_ListDialogContent-N_1_null,NEXUS_5,1.0,en].png | 4 ++-- ...ultipleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png | 4 ++-- ..._SingleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png | 4 ++-- ...ogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...logs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ..._null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png | 4 ++-- ...omposerLinkDialogCreateLink-N-5_6_null,NEXUS_5,1.0,en].png | 4 ++-- ...DialogCreateLinkWithoutText-N-6_7_null,NEXUS_5,1.0,en].png | 4 ++-- ...tComposerLinkDialogEditLink-N-7_8_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png | 4 ++-- ...rror.impl_null_AppErrorView-N-0_1_null,NEXUS_5,1.0,en].png | 4 ++-- 88 files changed, 176 insertions(+), 176 deletions(-) diff --git a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/CompoundColors.kt b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/CompoundColors.kt index fb5d44f880..f6b31cd678 100644 --- a/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/CompoundColors.kt +++ b/libraries/theme/src/main/kotlin/io/element/android/libraries/theme/compound/CompoundColors.kt @@ -34,7 +34,7 @@ internal val compoundColorsLight = SemanticColors( textOnSolidPrimary = LightDesignTokens.colorThemeBg, bgSubtlePrimary = LightDesignTokens.colorGray400, bgSubtleSecondary = LightDesignTokens.colorBgSubtleSecondaryLevel0, - bgCanvasDefault = LightDesignTokens.colorBgCanvasDefaultLevel1, + bgCanvasDefault = LightDesignTokens.colorThemeBg, bgCanvasDisabled = LightDesignTokens.colorGray200, bgActionPrimaryRest = LightDesignTokens.colorGray1400, bgActionPrimaryHovered = LightDesignTokens.colorGray1200, @@ -91,7 +91,7 @@ internal val compoundColorsDark = SemanticColors( bgSubtlePrimary = DarkDesignTokens.colorGray400, // The value DarkDesignTokens.colorBgSubtleSecondaryLevel0 is defined to colorThemeBg, this is not correct, so override the value here until this is fixed, bgSubtleSecondary = DarkDesignTokens.colorGray300, // DarkDesignTokens.colorBgSubtleSecondaryLevel0 - bgCanvasDefault = DarkDesignTokens.colorBgCanvasDefaultLevel1, + bgCanvasDefault = DarkDesignTokens.colorThemeBg, bgCanvasDisabled = DarkDesignTokens.colorGray200, bgActionPrimaryRest = DarkDesignTokens.colorGray1400, bgActionPrimaryHovered = DarkDesignTokens.colorGray1200, diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_0,NEXUS_5,1.0,en].png index 36cc0cbd16..87ac77de40 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a4093d0b443cf33d0bc58537b5a8e1e962259b96d850ecb6852ef225e9ab22b -size 26498 +oid sha256:498c6fcd0876693f7532e84cb8c765b44ee45344894ddfd5e1f906d9196ea43b +size 26343 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_1,NEXUS_5,1.0,en].png index 58ce5ae858..b3e0451cc0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4287f58375a0a9d7ebfbbac7812b2703412070ccc39329367dfe971a8c8ba4eb -size 28654 +oid sha256:b0b0ba104b45aa935732cdd6f14f46cb44bb842da2ec15b5aa3efd48495774a2 +size 28230 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_2,NEXUS_5,1.0,en].png index 454dc30c11..c12b0c44e8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[appnav.root_null_Root-N-3_4_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea97600c8e3b3a320737ac42e655ee7f0f62f6430eced2600af13ab2362160d3 -size 22066 +oid sha256:70e4e94aad224d9c3d71e40bb080030455d07d87c495ac4bdc3f54aff28a4eb4 +size 21916 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootView-N-4_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootView-N-4_5_null_2,NEXUS_5,1.0,en].png index 95e47794be..506ca9ec10 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootView-N-4_5_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.createroom.impl.root_null_CreateRoomRootView-N-4_5_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa52b86839fea55196c5f155c074098289cba8ca962d69b21293f07bfe391704 -size 27021 +oid sha256:5a8f4050dfe8a744191b439609b639bd5cd40b58a5b8af6762812677b64221be +size 26848 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-2_3_null,NEXUS_5,1.0,en].png index 7b1438b2c4..729467e765 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-2_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.ftue.impl.welcome_null_WelcomeView-N-2_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3a419038efa947fb7d168c2cfbdbf47f247b07405a451c0c0e560c182a9bedd -size 400195 +oid sha256:8d59dbc9d8b1db8de6bee9769a141c73077581df5c7381e55d1b631bdf21d4a2 +size 401867 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_2,NEXUS_5,1.0,en].png index 572122f7b9..10527ca1e9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18b2944aa47c83685c049e82f5c63127f7d14b789e72abf65be5f887231f6812 -size 49284 +oid sha256:fab7060aac1536a4206f7553064212ef8ffd33745fd14b54d10aa90cf3827c9c +size 48900 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_3,NEXUS_5,1.0,en].png index 4371030b33..280ae830be 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c361b187c660dd6a099539e56cb313472dd8e1c64bde6525470399f4275781b -size 49960 +oid sha256:4da2eb80232b80e99472a9c890843ff13b8b828b3e6a9d1a5a6c1a0da678a8be +size 49631 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_4,NEXUS_5,1.0,en].png index f815a0ffdf..48b6d10405 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:571c11b5cc2352f6eef293ec567b89925d83176700cb5b8f6b63c2211f742e71 -size 39769 +oid sha256:7927e9e5ed238f1d6113ba114826bd9d5bf30c10b7a5830234daa545ae56a3e3 +size 39708 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_5,NEXUS_5,1.0,en].png index f815a0ffdf..48b6d10405 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.invitelist.impl_null_InviteListView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:571c11b5cc2352f6eef293ec567b89925d83176700cb5b8f6b63c2211f742e71 -size 39769 +oid sha256:7927e9e5ed238f1d6113ba114826bd9d5bf30c10b7a5830234daa545ae56a3e3 +size 39708 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_1,NEXUS_5,1.0,en].png index 380d4fad99..d90b231578 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fb3709b90cf4ac34a4b516ac773516be94bcd7b12c282553a52a45750e8650f -size 19701 +oid sha256:f4be852024d6697c878859739c7eaef2402c3313900189eae8f293f43edbf26d +size 19469 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_2,NEXUS_5,1.0,en].png index 5150e06c43..9fa84b0182 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e121dde783ac747bd11763cf977f45137976ce1971457e37b9fd7bd6e7bb30a2 -size 28658 +oid sha256:3311445e4fec282ece72ae807fce1ece823c32a9f65a8235d019d3597a13cf63 +size 28551 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_3,NEXUS_5,1.0,en].png index adbb6453e0..631a5022bf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c01008d960a0591586ed903682a3b0796cd0de8195afd11781c9bca07b4249e -size 31474 +oid sha256:a3295ac1f8e5ac7e5a5dc04c8d1ec5777d7a9f609b30913b7234aff4034ba9a1 +size 31401 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_5,NEXUS_5,1.0,en].png index 9bd520b51a..375ed72288 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.leaveroom.api_null_LeaveRoomView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdc96749e49e01213ec696de561b2dc68f2590bf648a1fffe5fca9f4fb5d64fe -size 11186 +oid sha256:f9da3d0066a9f12a0826a20f760c9fddc5f58b1614406eff321a7fce1325803d +size 11165 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_1_null_1,NEXUS_5,1.0,en].png index 0e65ab4069..20dfafc434 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9973e81339db25c145bdd64e9949d117f0e9328e99a5e22bbd94a6b67169858 -size 38190 +oid sha256:8e7a461733795c789f0f6507874fbcb7d911b2b93ad0fd44a446d6882def8141 +size 38048 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_1_null_2,NEXUS_5,1.0,en].png index a6f3d6efa7..144654d0e5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.send_null_SendLocationView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8e7ec04162181a014b66018ce81b08ca1fa606d8dccce90fa562992d75a4084 -size 36622 +oid sha256:a6d34735c7ad55744a0309897d826d6fd2d935ee2ffb7a4f6192d1b5b5e9925e +size 36499 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationView-N-1_2_null_1,NEXUS_5,1.0,en].png index e62590cac2..63e6b130e1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10e98fafe17bba6162b6a86eb4a2f919afe21e45a25a01ae231f9f20c372aa97 -size 33166 +oid sha256:d983ed71433b3f833879df41a5cc167854ca939cce0b09fa99eb84a384975503 +size 33033 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationView-N-1_2_null_2,NEXUS_5,1.0,en].png index d4b63ac486..876e85417c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.location.impl.show_null_ShowLocationView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45ae0828c440e81e4bb262c4a0e72039038de95d6d9a6efe19db0f2f46787fc6 -size 31516 +oid sha256:42ca0b856f8793a8750fec69fd2f9d19ead83164ddb89de607b9997ccd302d93 +size 31400 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png index 2bdb7383d8..0f525564e4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9968ea696d5f88103b8ae6c03c58e87799dd9f22d831614d0563b343c49e931b -size 28582 +oid sha256:d28f9cb600a93d861fcfb866f876eec6cf422258e99dcdd66cbae2338d5406c2 +size 28289 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png index d0c1ab3c32..64d049871a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8c09be0dbf07d8593fd2f93881febdf3494bd439cb6f62adb78beee34b6c7e7 -size 25357 +oid sha256:f19ab48f8f165527b08e3b05df4b66d007c3d20dcf88daaf2f37793395737712 +size 25175 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png index 05e4d69148..4c852519f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68a54676b1f82425df6db8cb56b161950584ce9d433fc59630a16ac6288fc7d8 -size 31204 +oid sha256:6de6787421a27c30735584c6827465498fd3a4f28fe75a1f8cba613d62dd3d43 +size 30905 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png index 39854067a2..c81b8ccd2b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b0011f773767d2806b2269281415f910b4770bc0e95dff2a2a29a2c46f9e458 -size 41488 +oid sha256:836f26b162a031b14151511405ec0b9dd43fe1c37127a5ec05e277ab0e2ab667 +size 41054 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png index ac4cc89932..a6592c5832 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5b8afd4e3adca2e42463b66b5da449caaed61ee8ba9240a8a5e93f2c7ce75b4 -size 39061 +oid sha256:2c53928f35405266f99bbc4976f9c12915f47765289481b2cd65086925ea1b50 +size 38710 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-N-2_3_null,NEXUS_5,1.0,en].png index 2d088f837d..a173eb8fc4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-N-2_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.dialogs_null_SlidingSyncNotSupportedDialog-N-2_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:555f303cff58f52399fe6f8b8b6bc14cbea2cd71c084a66faa8bd2c0be2d0ff8 -size 19163 +oid sha256:9598f065a40451cbbb429bbc675700bc3800cd5640f4b06b17a7a2696253eed0 +size 18952 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_2,NEXUS_5,1.0,en].png index ca97e134d3..85b38120f9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.loginpassword_null_LoginPasswordView-N-6_7_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f1b84d70f67596af405a341d962d83cbd26e3925ffe86af35390e26c7eb0800 -size 27422 +oid sha256:a8fc290ee3547444b85fa14825b85571b1aa8293d5e951c9e62c89a880a9c85d +size 27393 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_2,NEXUS_5,1.0,en].png index 33f2589159..f6aaba2f2d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.login.impl.screens.waitlistscreen_null_WaitListView-N-8_9_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a212264caa4fa3c46e9bed62f62a01bf33f969f6da0da454899c3def9cc7e35a -size 64104 +oid sha256:a6687f1be6e8abab9ae846152cc4edeb5fe1becda4ec9bcf64c255ded9dc937a +size 63857 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png index 5bfc16a2b0..dc5cc2e1db 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4fa2444ab2941d044c71011bdb3ecaed621f125dd8752526b8485aaf80e1b63 -size 30747 +oid sha256:edf4ff593991f2cc88999034ff5ba80599d48ced6d23825a385ecf82d61d4686 +size 30383 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png index 5ef2025508..ee86ce04ea 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f200497bd6e06ed304a919c32b6622c711e8d570f7376be3518ab7fe0eb1a2f4 -size 29427 +oid sha256:5adffcc0b87707daba799293ec36b9e8202c72de2248835ed593b87fc82ea9b4 +size 29168 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png index da753d855a..76bb1a06d3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.attachments.preview_null_AttachmentsPreviewView_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb67d565e99a1c7068245e7e055ab4c3e3e13b2a59105dea4a53071ec6591257 -size 100262 +oid sha256:5f50bf3f0c23362762b9911352179d428821da26cbf5cdfc88dd31285ae8b478 +size 100137 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png index 1a4a05b147..b5cbe9f875 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.forward_null_ForwardMessagesView-N-2_3_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e967c504776e055c3ea45c0088a2c0636edb99fd6679dfe878eb3733ba116f8f -size 21815 +oid sha256:d1fee1e792bd0fdd7039d418f6f014e895e867de48126c1816fb035cab29981d +size 21676 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png index 8ff9e04c67..f2563cb7f7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97e5ebc64d490011841f813a4804f2ea702b0180c476bcc717bbb33512979d78 -size 114803 +oid sha256:16f345de6e480549895a0f77b7e066bcebbde69c501f1c58e3e82090e4dc0d87 +size 114646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_4,NEXUS_5,1.0,en].png index 60c95f487b..b1d670fde5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.report_null_ReportMessageView-N-7_8_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a58d7b8e0a1d631bf222d022d5c8432b654d8c934960d5b09c1864e4c349ddbc -size 34370 +oid sha256:cfe5805325dcd18b51fa123ee7961333a273b88d6f2a38c906bba8c70248c5eb +size 34396 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png index bf235fcdc6..223f192095 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9863144e5ee8fc010351509a11eac8d66332df97e56a8c5f81f2d0eb128fa368 -size 6806 +oid sha256:1bfffebea253932b08a308ac098a392910233e4a8524a898b459842849854c91 +size 6036 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png index 6e82a97ee0..b6cab390c3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e19c2813f5db4f4732d158e41211c2de175f9ab97c83296f719b5bfab615be2 -size 6775 +oid sha256:b846d71d9d0019f55ee34e4ef38375f1501663a855f2c05742239a08d5b7501d +size 6020 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png index 54510da6c4..1da72edd97 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a294dfa6003a4bba8a780169ced0df7b8e9bb5bd4113dd21d2a5467325f58023 -size 10396 +oid sha256:ca13f10ca5fd6194829c3cef02fa5cd0c00790f23c82d2cf70ac5d1d58babb54 +size 9656 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png index 903afbbde7..424216fd0a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba5a558037ae529515716a21b37070e5e04a9c0f6e0293a65a69cc3017a74b0d -size 10591 +oid sha256:1dfdaca0c643c81a281658b4cee4d0e938d542a57964c3831496d04db543da45 +size 9827 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png index bf6ad46dc4..aec364a0e6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28d5c962fb8528a96758e704d392a8444b1920dc36e49fd1bc684e5def2cf656 -size 10312 +oid sha256:ad2ccde01384563471412843a48180b7acb3ba9bbedb9af60f96d403e564073d +size 9566 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png index 32d7397228..cb45fe4553 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8adb090451574a5019a1e5c599fa6b2099035c830cc21cec816787e22fe4e690 -size 10329 +oid sha256:e32db0169cf50c2120a16efea3cb2b0f8a72f81ddce7a303c13bc2bf38c487ff +size 9653 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png index 8dbfba27e0..18a1430f5f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9fd731cc3718c850372e3106969ec99180113dca0e41e02e5133b46a4efa745 -size 10319 +oid sha256:93fb4175b173f065af362386e6c9026ce96fca5aee602d0dba9782fcbc0febb1 +size 9592 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png index 489b5572b6..df1c6e821a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72875fc0e93851a621ba9ded610c736133188e3609a9578209a90bce9e674641 -size 6333 +oid sha256:a0a6b88a5d7f001309dcb03da6151ac2949705b32a19301660a24fe02dc6fcdd +size 5562 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png index c258cbcb9a..9a70f58bb9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:393205bc33da82255be0f2f19846bdda87baa5f97ca7c623681ad9537fa3e677 -size 6733 +oid sha256:2f8562e1e599e16ecc05ec0471de70b9de9a88b347f622bc723f21cb5379b102 +size 6033 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png index d240797215..5c9912a57e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ecc36796f0404123f5ec125b4631535e50e1ad6ad70f5d933ae43879e580121 -size 6740 +oid sha256:cac1f8a26b53bc6867ed54095de6ee73b840fb0dccf056f99678bfa9df126d7b +size 5993 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png index 75742f88af..aa9de6ea89 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e63f75edead31ff90194fedcdd6122503705d758959961466e652489f0819b0 -size 7879 +oid sha256:d4a2824fdc49adb0e3fc155e7da319414544a87dc083bdece78e85ba72669d78 +size 7155 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png index 41c4a39ab9..4e5b8e146d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:636a174a411d9d76212d1fe51717b147a2d0586ab55a73a29c42c3d139617f6e -size 7800 +oid sha256:3817e348ba2775528e3805ad6f5eed044f82b241f73692180c0638c95fb79fa2 +size 7076 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png index be1c277966..32b8d33a13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:172b277c375702a57bdb204a0addadec0baa86236eab9130eb44b125bd615058 -size 7445 +oid sha256:fd21f1c5ad2616b698a386d78d5efe0ab40aa1959382ed2a568c10441d093a8f +size 6692 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png index 73cf00afee..dbee328561 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eeb21741d4caccb07cc93db32887ec423d1c149a5b769bc3cd4cd0d88aa566d -size 7624 +oid sha256:07f52161378dfab92ffb76b860680390ccb5dfcc05347c5fd99504c806dd3210 +size 6930 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png index 6a67feef69..a0f8df916c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceView-N-41_42_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f3eb73f4a4ea4fb2e7b21d88393fab914b51cc63d9e01cd67fd4879ad856afe -size 7899 +oid sha256:39905ed8db6cbf7ba5400f0b0455785703667ce9c47d1dfb59db217f40f4d507 +size 7162 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png index 21c8e08e03..a954c65665 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemVoiceViewUnified-N-42_43_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d277630d19b10150d18a660c06d38c02ce19cd5079667bd58cca385a7b540ae2 -size 54302 +oid sha256:0c50b273463475a1ecdeb1393f688da883448be9fefa28b8ea15232a5ef7c601 +size 45417 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png index 08ec54b681..c608f99a68 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_null_TimelineView-N-8_9_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3be541cea4b8775aa141fd0f5e8d9b83f295684b4e9c8a070e9b1a4511e1531 -size 55884 +oid sha256:be817a2061d43e210c105b193f1baed6060c51f11d97f45d0593cb40ab7acd98 +size 56403 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png index b64203f138..2b1a73c36a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:010d76d537104b914a35cdb783c5cfcc1953a470d2d9bc23781c8837c9558f5f -size 54122 +oid sha256:62504045bfd1b2214e6c80d266ba1c92f750ddfff70038c55f2ea9f59e6fc3a1 +size 54127 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png index 578ad1301e..5f884c0794 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1a0280d6321b754e5e36d25eefc2d5c7192af16e52ae1304b4e7c57a7824e19 -size 51611 +oid sha256:090ea31e1183d552111c3c92c535cfbb316866a9832d8f26fa1f7bda6ce03cda +size 51042 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png index ad11262fbe..5ef67cf379 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f3a764349e13ebe7205ff4d0b04a02f64c6eee5d65bb3b9cc62ac1d721d2d39 -size 54944 +oid sha256:43476c4f4f2ab6eef1286ad6083d9ea01659ea7d96a49fb0f6ea5ac76158369d +size 54860 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_1_null_2,NEXUS_5,1.0,en].png index 12d0641cff..e6182ffbae 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f84f63c2da7fc27bb58d7c7c24c9b131a5ebf8962c21c513806936fd0ea2a71 -size 36179 +oid sha256:8b63e78ca18401ac3df3c66057c888012d1b5b0eda0befc19ff5c22e4d57f51d +size 36011 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_3,NEXUS_5,1.0,en].png index 4b8fd2030c..18a4bfab1c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications.edit_null_EditDefaultNotificationSettingView-N-8_9_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0b0df0468cc575ff14b9dc07ca6d4a61bb19ad709ead861a412489499e1bfd5 -size 32201 +oid sha256:a8804c266b0a55c0d3a4e716aa6d9c75f82f2bfafbbd54b8f9475230e20ff863 +size 32294 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsView-N-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsView-N-5_6_null_2,NEXUS_5,1.0,en].png index 62424895b4..b484ff77bd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsView-N-5_6_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.notifications_null_NotificationSettingsView-N-5_6_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3a3796c755d202361d4eacf2be151a9b579b588242d55706dbc35e26b13b081 -size 44306 +oid sha256:2fc6852036964b9519a9be8eb171b6fce4611f9575208d47f70e21e945bb18da +size 44401 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.api.crash_null_CrashDetectionView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.api.crash_null_CrashDetectionView-N-0_1_null,NEXUS_5,1.0,en].png index 1ff9a6a1e4..38cf0f4fe1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.api.crash_null_CrashDetectionView-N-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.api.crash_null_CrashDetectionView-N-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b07ab0285059dfd7de27e9f3667d79a32253e95ce6ec40150428fb63890c19d8 -size 24681 +oid sha256:5b4cf8785b873a930a5c8b21862b54f9b038e0a696f73bc8d52c11d06a3744e9 +size 24513 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.api.detection_null_RageshakeDialogContent-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.api.detection_null_RageshakeDialogContent-N-1_2_null,NEXUS_5,1.0,en].png index 19de0eb9c4..793d222749 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.api.detection_null_RageshakeDialogContent-N-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.rageshake.api.detection_null_RageshakeDialogContent-N-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:845aeb2a5ef40a64da13d1467ee0cbf8e151bffa31b7e8de632fd86d71247d81 -size 26685 +oid sha256:fe5847cbe6b2757a7ef343394f0e2d10c5d28a075e3d127cb539b76ca43c2f75 +size 26289 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditView-N-0_1_null_6,NEXUS_5,1.0,en].png index a3f61a205d..8e77061376 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditView-N-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.edit_null_RoomDetailsEditView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c85f84bc8a8ed279785e1213f7f930df9ccdea34e658a79ca88fd06d2d3c0a39 -size 24407 +oid sha256:2be03a264d63f3e9d403cf0db50b099ce5d973acb17689303a83b9c332887376 +size 24376 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png index acd834cafd..60ce7700f0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5b98b3001442f362540855822813e1d951b3e2bf2bef74189b0182e6cefd6dd -size 36963 +oid sha256:fb6e059c2d95b2efe028f9995e90dccb42938eb56cfcc228d37c01427a1151e8 +size 36788 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png index 52e42ff13a..591358df1f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:276afc61d84cdd461e00121e9c38756b91a0f422c848772d52492c9dfe3c4890 -size 28494 +oid sha256:be13b77a299b2e55d46b51a277c1e1fbb668ee18e54cafbe96f49ef762e39864 +size 28151 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_3,NEXUS_5,1.0,en].png index 4c2b14932b..d3095ecc13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bf1fc0c9605c5e8e18fb7a45ec3a7ed0ff2f9920b85582800098929f0521fbd -size 36512 +oid sha256:ee987e41ce37ff52cf8265fc2ebddfff18b6942e624d40b418033a27531874ed +size 36623 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_5,NEXUS_5,1.0,en].png index 4c2b14932b..d3095ecc13 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.notificationsettings_null_RoomNotificationSettings-N-4_5_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bf1fc0c9605c5e8e18fb7a45ec3a7ed0ff2f9920b85582800098929f0521fbd -size 36512 +oid sha256:ee987e41ce37ff52cf8265fc2ebddfff18b6942e624d40b418033a27531874ed +size 36623 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png index 39812bd3f0..d5e927ad65 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ab1a52d75b027259c855c63b2b626f286899963b251e5ac9540478b0f7988c3 -size 42813 +oid sha256:97c8f3ca9f5f9f4c89930b1279261125a548d5770a361f0f9749904212bb3fb3 +size 42305 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png index c1577d4d23..d13b3cc27f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36152a1506067dafcc6591d0ef0a29e68b96df40070eb8f039fac484f152f6b7 -size 27091 +oid sha256:d50b5caf3f62d349985102f951a6bf679923b3726f420c6d55e128d6763d5b40 +size 27043 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png index 7e318fb1a8..4357469394 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b19e0f154e435885299c579360112ecf04c14f67f141ebe855a7dc32052f962a -size 18636 +oid sha256:e10389377cd21f189794e8a91cecee43b3c44a0030d5f312558dfef7add80dd1 +size 18548 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png index 1879d86aa6..1081be3ddc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01455558e19098ab432b0b7cb48c1be2e9d2b2d0b1e97035143687d202687b13 -size 29643 +oid sha256:dab45727bd828a76376b796e0d76794d7fb42ac80bc8c721b661eaf0926bb382 +size 29544 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png index 1481c6abdc..5ed4e0fa69 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:035ef744899cd61549684482dccb8845018e4f574bdfdcdb57d33fb98a23ef3b -size 45743 +oid sha256:8be58c4edb9be3cadbd15ac98f8eabbf4ed72af5cac31f0d3d0a1f18c86c5046 +size 45221 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png index 1481c6abdc..5ed4e0fa69 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:035ef744899cd61549684482dccb8845018e4f574bdfdcdb57d33fb98a23ef3b -size 45743 +oid sha256:8be58c4edb9be3cadbd15ac98f8eabbf4ed72af5cac31f0d3d0a1f18c86c5046 +size 45221 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png index 4d9869c5eb..0e8fee609d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ConfirmationDialog_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e13461ce6fe7e873fa2796520cf107e6e041559b43c8d911ba78848de34b398 -size 24585 +oid sha256:0dc9f59b773351ab464b895c4da80937cb357a4d1f478390e0cc6649d43a6551 +size 24327 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png index 0146de16cc..9ad73606ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_ErrorDialog_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eebe29b4f5b0aa38316bb54e439f9ab541185251deffbae07ba14342b453c5cb -size 17528 +oid sha256:293a9791a9f8fa8fa81c499870fbac9a45236d5c0cb6d7a5e0541171c9f16a77 +size 17530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png index 915f6f8539..b09b84e364 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_Dialogs_RetryDialog_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a19f93d6d31a074b2e5c1d691505881e8424361f1d244366bc6ee5fbd5de15f6 -size 23225 +oid sha256:e4dcf01ec38f12deecca017130ecf723ab36cb2fbc92e4eed82bcf0c1a8610aa +size 23138 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_ListDialogContent-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_ListDialogContent-N_1_null,NEXUS_5,1.0,en].png index 653d02dabb..ae2f7fa24c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_ListDialogContent-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_ListDialogContent-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd5cccdc9db4251ae5a51061fc0d399a5d08806183c693a68a0b57b52f92b20e -size 20003 +oid sha256:a7a7c6db1c2ceae0748793eda9b93602626d52294f041466ca33cb8681b94d87 +size 19700 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_MultipleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_MultipleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png index f92655c313..5ec2d86b28 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_MultipleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_MultipleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f540b6b0f761949348fbf65596103d482b0f3c29eb8c99750756dce1c40e674e -size 28692 +oid sha256:8bc428ed9ed3c09556ade7fe797e843dad711d8c478936b6dc124933e3c8ce47 +size 28366 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_SingleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_SingleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png index de25110b80..ffa5f3c462 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_SingleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.components.dialogs_null_SingleSelectionDialogContent-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa6c275983acd5902a73fecc03a3050f1dbf70c272557bf8240c7bc5ec6c1a45 -size 19809 +oid sha256:001a87e1c34d96910971d1071479321d2ca9ed386ebf19de45ad79410d7a60ac +size 19581 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png index 8bc5d4dfb5..89a813e066 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithdestructivebutton_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3079a82a70fc20feac8c694a8571eac5dac9e2dea800bd7ca01628fe7c90d29 -size 33019 +oid sha256:2998ea82194995d26557fca02f9258ebdf3645e002b353f3d9e10cb481eae0cf +size 32981 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png index 32d5b45a6b..8fc2b551af 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithonlymessageandokbutton_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a38a4a04c3527e9d1bb7efc3c4b91f6cfbd4eab93c84d316e68a4fc558e137cd -size 51533 +oid sha256:aaa305ec281d5d5a98f44fad28eff7128b26f66ec0724c23bc2b5d341bba615b +size 51759 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png index a2744cb3ed..039267ab46 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitle,iconandokbutton_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:174291d52614bc86f3cf527b24156656f71d6ae616236f1bef5c95b8272fd258 -size 57938 +oid sha256:4b8d0f0a54f0124f7eb021fa6f8463390d55863cc1121287687c53ae6fed41ac +size 58156 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png index d090dc2fa8..31cbdfdf03 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_Dialogs_Dialogwithtitleandokbutton_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9980d0479b405e3b3215628ae92227c02423b7839c2754bb4b230da549e82cbe -size 56305 +oid sha256:423a0b7cb2153044bc1e4b32d5153c38f51a0d946c21c36d61a28e7bfe692c1c +size 56464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png index c91ef3380a..3a963029b6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7a12d96b619106be6075b39343582044062affaa58c3fe46cd04c570b08c34d -size 27650 +oid sha256:5ad31f0c4f91caae057ee5b5256a1fa108c998c829c00fe8a1274ea757bf4793 +size 27348 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png index 54dbd92c83..7ba9368ad2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3e6a1cf98bca50a1949cdaf27663cfbe5d2bb6f829a106f6a4291cb4b643ce1 -size 26805 +oid sha256:18f63eb6d1891ebf4efc5679adf52ca1e3d7cdbcbc30f36be5db8e55c36589b4 +size 26530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png index 27a3608686..700e8ddcae 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7c42827f60bbe46846284cf135c15402abef93a404fc8ffa85cb27fb386d744 -size 27421 +oid sha256:c694d1867634284d946cc0ebcbe91b9997a523ab7ebfb9923f113d8c07753f95 +size 27044 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png index 0a1d99b585..a810217e45 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3775126d27af04166dac57b67e14d4f0bd0ac2baf76a00bc665bbc8e30c7c716 -size 21025 +oid sha256:9239138fd8813cb7b28baa93a8ab7ccc60c5b9bb1f330b7e4e2ec92d7012648f +size 20730 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png index b4733b4baa..33f750d708 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fca03bd3111ed7ee360ef60211c790aac9bb21dc3dcac8d0bcb8f3f315d6dd93 -size 19232 +oid sha256:93a4628b19a5d8a5b1f064a0c5e6d8d90e2690481b10b34c1beca2cc84b3d34c +size 20848 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-N-5_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-N-5_6_null,NEXUS_5,1.0,en].png index 24f5ef4644..1eee8a30f6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-N-5_6_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLink-N-5_6_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b29c850d091691b8757af57e5d454d801509f5f9a205f7d842e6b16551e16238 -size 14810 +oid sha256:deaef5e0582fa46d67f96ce5b50fb669e59ac5f76ffd079a93c9476cd1d4fc90 +size 14417 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-N-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-N-6_7_null,NEXUS_5,1.0,en].png index 4e20df75ba..5ff6a58533 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-N-6_7_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogCreateLinkWithoutText-N-6_7_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccda12020d51dafce20ad815b6b6162adce5d005c2a908b951fa9e97bede8a7c -size 13467 +oid sha256:b723aef3136b03ade441ebbc26b3c6705185a3d2f4918841d57f3db5ccac5c91 +size 13174 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-N-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-N-7_8_null,NEXUS_5,1.0,en].png index b5d4436cd8..c2a890e33c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-N-7_8_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerLinkDialogEditLink-N-7_8_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc7484e27dd08b2d477f3b07c34558a0459ec7f8e500c16c5e94417d33e7cd88 -size 17448 +oid sha256:787cc5ee6d40cb33b2e462a9c5a679e3bf6bd178f68f8bdaf83fc9d790dc57b1 +size 17121 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png index 7a466108e4..7c287d4d9e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:875a8c7f394a0cd4c7944bf5cbf969a5e54108b8c3edee40c193d705c9ba3719 -size 25065 +oid sha256:b03f6a351e4f6a22ca78b1f7b703568cef9855920ae03089c300a60c0fc593fb +size 26401 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_null_AppErrorView-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_null_AppErrorView-N-0_1_null,NEXUS_5,1.0,en].png index 1867d19a69..28c7fb7aac 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_null_AppErrorView-N-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[services.apperror.impl_null_AppErrorView-N-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97efcd302d41f069cf0d75ba3c9d44615214ac9a3f3e2af95694ff10edbeda9b -size 20270 +oid sha256:e2fc386d065ab90b1f28b2fe4974546db4faf3c015c20668be5db137385209bd +size 20111 From 38ae17154893eee1f2f8103736f4229f6599e2d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 19:16:00 +0000 Subject: [PATCH 248/281] Update dependency com.google.firebase:firebase-bom to v32.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 90c691b477..67d2e14d0f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.4.1" +google_firebase_bom = "com.google.firebase:firebase-bom:32.5.0" # AndroidX androidx_core = { module = "androidx.core:core", version.ref = "core" } From 12efed3a94bc34c7b257a3842c164032fce7532f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Oct 2023 18:48:23 +0200 Subject: [PATCH 249/281] Move SecureBackupConfig to the appconfig module. --- .../kotlin/io/element/android/appconfig}/SecureBackupConfig.kt | 3 +-- features/securebackup/impl/build.gradle.kts | 1 + .../features/securebackup/impl/root/SecureBackupRootNode.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename {features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl => appconfig/src/main/kotlin/io/element/android/appconfig}/SecureBackupConfig.kt (86%) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/SecureBackupConfig.kt similarity index 86% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupConfig.kt rename to appconfig/src/main/kotlin/io/element/android/appconfig/SecureBackupConfig.kt index 045a320e4a..61f015d239 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/SecureBackupConfig.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package io.element.android.features.securebackup.impl +package io.element.android.appconfig -// TODO Move to appconfig module when it will be available object SecureBackupConfig { const val LearnMoreUrl: String = "https://element.io/help#encryption5" } diff --git a/features/securebackup/impl/build.gradle.kts b/features/securebackup/impl/build.gradle.kts index 68e8dc066f..a3509b65dd 100644 --- a/features/securebackup/impl/build.gradle.kts +++ b/features/securebackup/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { anvil(projects.anvilcodegen) implementation(projects.anvilannotations) + implementation(projects.appconfig) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt index 676214e698..0723568881 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootNode.kt @@ -27,7 +27,7 @@ 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.SecureBackupConfig +import io.element.android.appconfig.SecureBackupConfig import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) From 19236c0602338e63b99f4cd66dd328bf51d02f68 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Oct 2023 18:50:44 +0200 Subject: [PATCH 250/281] Replace it by when. --- .../features/roomlist/impl/RoomListView.kt | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) 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 2d7ea1e4b1..afb110e4de 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 @@ -52,8 +52,8 @@ import io.element.android.features.roomlist.impl.components.RoomListTopBar import io.element.android.features.roomlist.impl.components.RoomSummaryRow import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.search.RoomListSearchResultView -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon @@ -190,19 +190,22 @@ private fun RoomListContent( .nestedScroll(nestedScrollConnection), state = lazyListState, ) { - if (state.displayVerificationPrompt) { - item { - RequestVerificationHeader( - onVerifyClicked = onVerifyClicked, - onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } - ) + when { + state.displayVerificationPrompt -> { + item { + RequestVerificationHeader( + onVerifyClicked = onVerifyClicked, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } + ) + } } - } else if (state.displayRecoveryKeyPrompt) { - item { - ConfirmRecoveryKeyBanner( - onContinueClicked = onOpenSettings, - onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } - ) + state.displayRecoveryKeyPrompt -> { + item { + ConfirmRecoveryKeyBanner( + onContinueClicked = onOpenSettings, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } + ) + } } } From dc1bbb0145c22b76857fc1f1a0d3ea0f62cb9936 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Oct 2023 18:57:48 +0200 Subject: [PATCH 251/281] Enter recovery key: add Done ime action. --- .../enter/SecureBackupEnterRecoveryKeyView.kt | 7 +++++- .../impl/setup/SecureBackupSetupView.kt | 1 + .../impl/setup/views/RecoveryKeyView.kt | 23 ++++++++++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) 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 3fb95e7310..2e5157ddba 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 @@ -75,6 +75,9 @@ fun SecureBackupEnterRecoveryKeyView( state = state, onChange = { state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it)) + }, + onSubmit = { + state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit) }) } } @@ -112,13 +115,15 @@ private fun BottomMenu( @Composable private fun Content( state: SecureBackupEnterRecoveryKeyState, - onChange: ((String) -> Unit)?, + onChange: (String) -> Unit, + onSubmit: () -> Unit, ) { RecoveryKeyView( modifier = Modifier.padding(top = 52.dp), state = state.recoveryKeyViewState, onClick = null, onChange = onChange, + onSubmit = onSubmit, ) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt index 0e1fa3b824..880b1a687f 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt @@ -192,6 +192,7 @@ private fun Content( state = state, onClick = onClick, onChange = null, + onSubmit = null, ) } 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 24e70d3b8c..e243889c90 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 @@ -25,12 +25,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -52,6 +55,7 @@ internal fun RecoveryKeyView( state: RecoveryKeyViewState, onClick: (() -> Unit)?, onChange: ((String) -> Unit)?, + onSubmit: (() -> Unit)?, modifier: Modifier = Modifier, ) { Column( @@ -63,7 +67,7 @@ internal fun RecoveryKeyView( modifier = Modifier.padding(start = 16.dp), style = ElementTheme.typography.fontBodyMdRegular, ) - RecoveryKeyContent(state, onClick, onChange) + RecoveryKeyContent(state, onClick, onChange, onSubmit) RecoveryKeyFooter(state) } } @@ -73,11 +77,12 @@ private fun RecoveryKeyContent( state: RecoveryKeyViewState, onClick: (() -> Unit)?, onChange: ((String) -> Unit)?, + onSubmit: (() -> Unit)?, ) { when (state.recoveryKeyUserStory) { RecoveryKeyUserStory.Setup, RecoveryKeyUserStory.Change -> RecoveryKeyStaticContent(state, onClick) - RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange) + RecoveryKeyUserStory.Enter -> RecoveryKeyFormContent(state, onChange, onSubmit) } } @@ -143,8 +148,13 @@ private fun RecoveryKeyStaticContent( } @Composable -private fun RecoveryKeyFormContent(state: RecoveryKeyViewState, onChange: ((String) -> Unit)?) { +private fun RecoveryKeyFormContent( + state: RecoveryKeyViewState, + onChange: ((String) -> Unit)?, + onSubmit: (() -> Unit)?, +) { onChange ?: error("onChange should not be null") + onSubmit ?: error("onSubmit should not be null") val recoveryKeyVisualTransformation = remember { RecoveryKeyVisualTransformation() } @@ -155,6 +165,12 @@ private fun RecoveryKeyFormContent(state: RecoveryKeyViewState, onChange: ((Stri onValueChange = onChange, enabled = state.inProgress.not(), visualTransformation = recoveryKeyVisualTransformation, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { onSubmit() } + ), label = { Text(text = stringResource(id = R.string.screen_recovery_key_confirm_key_placeholder)) } ) } @@ -217,5 +233,6 @@ internal fun RecoveryKeyViewPreview( state = state, onClick = {}, onChange = {}, + onSubmit = {}, ) } From 0db487fa4234f5825f11ebd835da2d75bb734b47 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 30 Oct 2023 11:03:29 +0100 Subject: [PATCH 252/281] Secure backup and sign out: add TopBar with Back button. --- .../features/logout/impl/LogoutNode.kt | 1 + .../features/logout/impl/LogoutView.kt | 16 +++++++++-- .../impl/disable/SecureBackupDisableNode.kt | 1 + .../impl/disable/SecureBackupDisableView.kt | 14 +++++++++- .../impl/enable/SecureBackupEnableNode.kt | 1 + .../impl/enable/SecureBackupEnableView.kt | 14 +++++++++- .../enter/SecureBackupEnterRecoveryKeyNode.kt | 3 ++- .../enter/SecureBackupEnterRecoveryKeyView.kt | 14 +++++++++- .../impl/setup/SecureBackupSetupNode.kt | 1 + .../impl/setup/SecureBackupSetupView.kt | 27 ++++++++++++++++++- .../SecureBackupSetupViewChangePreview.kt | 1 + 11 files changed, 86 insertions(+), 7 deletions(-) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt index 8c4a605222..ccb00b958f 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutNode.kt @@ -58,6 +58,7 @@ class LogoutNode @AssistedInject constructor( state = state, onChangeRecoveryKeyClicked = ::onChangeRecoveryKeyClicked, onSuccessLogout = { onSuccessLogout(activity, it) }, + onBackClicked = ::navigateUp, modifier = modifier, ) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt index d0635af31e..7fc05886d0 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -32,6 +33,7 @@ import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMo 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.ProgressDialog +import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -39,16 +41,19 @@ import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogoutView( state: LogoutState, onChangeRecoveryKeyClicked: () -> Unit, + onBackClicked: () -> Unit, onSuccessLogout: (logoutUrlResult: String?) -> Unit, modifier: Modifier = Modifier, ) { @@ -56,6 +61,12 @@ fun LogoutView( HeaderFooterPage( modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { BackButton(onClick = onBackClicked) }, + title = {}, + ) + }, header = { HeaderContent(state = state) }, @@ -134,7 +145,7 @@ private fun HeaderContent( else -> null } - val paddingTop = 60.dp + val paddingTop = 0.dp IconTitleSubtitleMolecule( modifier = modifier.padding(top = paddingTop), iconResourceId = CommonDrawables.ic_key, @@ -219,6 +230,7 @@ internal fun LogoutViewPreview( LogoutView( state, onChangeRecoveryKeyClicked = {}, - onSuccessLogout = {} + onSuccessLogout = {}, + onBackClicked = {}, ) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt index 42f1c81f57..6e3d69fe7f 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableNode.kt @@ -40,6 +40,7 @@ class SecureBackupDisableNode @AssistedInject constructor( state = state, modifier = modifier, onDone = ::navigateUp, + onBackClicked = ::navigateUp, ) } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt index 290086849f..133043d064 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier @@ -33,6 +34,7 @@ import io.element.android.libraries.architecture.Async 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.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview @@ -40,13 +42,16 @@ 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.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.theme.ElementTheme +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SecureBackupDisableView( state: SecureBackupDisableState, onDone: () -> Unit, + onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { LaunchedEffect(state.disableAction) { @@ -56,6 +61,12 @@ fun SecureBackupDisableView( } HeaderFooterPage( modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { BackButton(onClick = onBackClicked) }, + title = {}, + ) + }, header = { HeaderContent() }, @@ -95,7 +106,7 @@ private fun HeaderContent( modifier: Modifier = Modifier, ) { IconTitleSubtitleMolecule( - modifier = modifier.padding(top = 60.dp), + modifier = modifier.padding(top = 0.dp), iconResourceId = CommonDrawables.ic_key_off, title = stringResource(id = R.string.screen_key_backup_disable_title), subTitle = stringResource(id = R.string.screen_key_backup_disable_description), @@ -158,5 +169,6 @@ internal fun SecureBackupDisableViewPreview( SecureBackupDisableView( state = state, onDone = {}, + onBackClicked = {}, ) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt index 34d03881d6..9966dd0df0 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableNode.kt @@ -40,6 +40,7 @@ class SecureBackupEnableNode @AssistedInject constructor( state = state, modifier = modifier, onDone = ::navigateUp, + onBackClicked = ::navigateUp, ) } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt index feaeb01d02..12e02663ec 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enable/SecureBackupEnableView.kt @@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl.enable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier @@ -29,16 +30,20 @@ import io.element.android.libraries.architecture.Async 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.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog 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.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SecureBackupEnableView( state: SecureBackupEnableState, onDone: () -> Unit, + onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { LaunchedEffect(state.enableAction) { @@ -48,6 +53,12 @@ fun SecureBackupEnableView( } HeaderFooterPage( modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { BackButton(onClick = onBackClicked) }, + title = {}, + ) + }, header = { HeaderContent() }, @@ -68,7 +79,7 @@ private fun HeaderContent( modifier: Modifier = Modifier, ) { IconTitleSubtitleMolecule( - modifier = modifier.padding(top = 60.dp), + modifier = modifier.padding(top = 0.dp), iconResourceId = CommonDrawables.ic_key, title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable), subTitle = null, @@ -99,5 +110,6 @@ internal fun SecureBackupEnableViewPreview( SecureBackupEnableView( state = state, onDone = {}, + 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 dcea7dfdb0..79576da8a1 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 @@ -50,7 +50,8 @@ class SecureBackupEnterRecoveryKeyNode @AssistedInject constructor( onDone = { coroutineScope.postSuccessSnackbar() navigateUp() - } + }, + onBackClicked = ::navigateUp, ) } 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 2e5157ddba..c3e20e2312 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 @@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl.enter import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier @@ -30,17 +31,21 @@ import io.element.android.libraries.architecture.Async 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.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog 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.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.ui.strings.CommonStrings +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SecureBackupEnterRecoveryKeyView( state: SecureBackupEnterRecoveryKeyState, onDone: () -> Unit, + onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { when (state.submitAction) { @@ -59,6 +64,12 @@ fun SecureBackupEnterRecoveryKeyView( HeaderFooterPage( modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { BackButton(onClick = onBackClicked) }, + title = {}, + ) + }, header = { HeaderContent() }, @@ -87,7 +98,7 @@ private fun HeaderContent( modifier: Modifier = Modifier, ) { IconTitleSubtitleMolecule( - modifier = modifier.padding(top = 60.dp), + modifier = modifier.padding(top = 0.dp), iconResourceId = CommonDrawables.ic_key, title = stringResource(id = R.string.screen_recovery_key_confirm_title), subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description), @@ -135,5 +146,6 @@ internal fun SecureBackupEnterRecoveryKeyViewPreview( SecureBackupEnterRecoveryKeyView( state = state, onDone = {}, + onBackClicked = {}, ) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt index 15effd9045..84cee69110 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupNode.kt @@ -60,6 +60,7 @@ class SecureBackupSetupNode @AssistedInject constructor( coroutineScope.postSuccessSnackbar() navigateUp() }, + onBackClicked = ::navigateUp, modifier = modifier, ) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt index 880b1a687f..6c35a979e0 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt @@ -16,8 +16,10 @@ package io.element.android.features.securebackup.impl.setup +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -32,24 +34,42 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten 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.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog 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.IconSource import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.ui.strings.CommonStrings +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SecureBackupSetupView( state: SecureBackupSetupState, onDone: () -> Unit, + onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current + val canGoBack = state.canGoBack() + BackHandler(enabled = canGoBack) { + onBackClicked() + } HeaderFooterPage( modifier = modifier, + topBar = { + TopAppBar( + navigationIcon = { + if (canGoBack) { + BackButton(onClick = onBackClicked) + } + }, + title = {}, + ) + }, header = { HeaderContent(state = state) }, @@ -109,6 +129,10 @@ fun SecureBackupSetupView( } } +private fun SecureBackupSetupState.canGoBack(): Boolean { + return recoveryKeyViewState.formattedRecoveryKey == null +} + @Composable private fun HeaderContent( state: SecureBackupSetupState, @@ -136,7 +160,7 @@ private fun HeaderContent( stringResource(id = R.string.screen_recovery_key_save_description) } IconTitleSubtitleMolecule( - modifier = modifier.padding(top = 60.dp), + modifier = modifier.padding(top = 0.dp), iconResourceId = CommonDrawables.ic_key, title = title, subTitle = subTitle, @@ -204,5 +228,6 @@ internal fun SecureBackupSetupViewPreview( SecureBackupSetupView( state = state, onDone = {}, + onBackClicked = {}, ) } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt index 0f7408700f..31efdfa3a1 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt @@ -33,5 +33,6 @@ internal fun SecureBackupSetupViewChangePreview( recoveryKeyViewState = state.recoveryKeyViewState.copy(recoveryKeyUserStory = RecoveryKeyUserStory.Change), ), onDone = {}, + onBackClicked = {}, ) } From 90b377b3a52853232acce5aac37e70270fb9612f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 30 Oct 2023 15:22:44 +0100 Subject: [PATCH 253/281] Secure Storage: improve API for `waitForBackupUploadSteadyState()` --- .../features/logout/impl/LogoutPresenter.kt | 7 +++++-- .../features/logout/impl/LogoutPresenterTest.kt | 15 ++++++++++++--- .../matrix/api/encryption/EncryptionService.kt | 6 +++--- .../impl/encryption/RustEncryptionService.kt | 15 ++++++++++----- .../test/encryption/FakeEncryptionService.kt | 12 +++++++----- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt index 8d97df3801..982fb0d796 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -46,13 +47,15 @@ class LogoutPresenter @Inject constructor( mutableStateOf(Async.Uninitialized) } - val backupUploadState by encryptionService.backupUploadStateStateFlow.collectAsState() + val backupUploadState: BackupUploadState by remember { + encryptionService.waitForBackupUploadSteadyState() + } + .collectAsState(initial = BackupUploadState.Unknown) var showLogoutDialog by remember { mutableStateOf(false) } var isLastSession by remember { mutableStateOf(false) } LaunchedEffect(Unit) { isLastSession = encryptionService.isLastDevice().getOrNull() ?: false - encryptionService.waitForBackupUploadSteadyState() } fun handleEvents(event: LogoutEvents) { diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index fe703780ba..150401c902 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -28,6 +28,8 @@ import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -73,6 +75,15 @@ class LogoutPresenterTest { @Test fun `present - initial state - backing up`() = runTest { val encryptionService = FakeEncryptionService() + encryptionService.givenWaitForBackupUploadSteadyStateFlow( + flow { + emit(BackupUploadState.Waiting) + delay(1) + emit(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) + delay(1) + emit(BackupUploadState.Done) + } + ) val presenter = createLogoutPresenter( encryptionService = encryptionService ) @@ -81,13 +92,11 @@ class LogoutPresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.isLastSession).isFalse() - assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) + assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Waiting) assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) - encryptionService.emitBackupUploadState(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) val state = awaitItem() assertThat(state.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) - encryptionService.emitBackupUploadState(BackupUploadState.Done) val doneState = awaitItem() assertThat(doneState.backupUploadState).isEqualTo(BackupUploadState.Done) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index b11cc3f9ab..9652c53678 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -16,12 +16,12 @@ package io.element.android.libraries.matrix.api.encryption +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface EncryptionService { val backupStateStateFlow: StateFlow val recoveryStateStateFlow: StateFlow - val backupUploadStateStateFlow: StateFlow val enableRecoveryProgressStateFlow: StateFlow suspend fun enableBackups(): Result @@ -46,7 +46,7 @@ interface EncryptionService { suspend fun fixRecoveryIssues(recoveryKey: String): Result /** - * Observe [backupUploadStateStateFlow] to get progress. + * Wait for backup upload steady state. */ - suspend fun waitForBackupUploadSteadyState(): Result + fun waitForBackupUploadSteadyState(): Flow } 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 9b1e3909d7..b1089a215d 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 @@ -22,7 +22,10 @@ import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.BackupStateListener import org.matrix.rustcomponents.sdk.BackupSteadyStateListener @@ -50,7 +53,6 @@ internal class RustEncryptionService( override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(service.backupState().let(backupStateMapper::map)) override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(service.recoveryState().let(recoveryStateMapper::map)) override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) - override val backupUploadStateStateFlow: MutableStateFlow = MutableStateFlow(BackupUploadState.Unknown) fun start() { service.backupStateListener(object : BackupStateListener { @@ -94,16 +96,19 @@ internal class RustEncryptionService( } } - override suspend fun waitForBackupUploadSteadyState( - ): Result = withContext(dispatchers.io) { - runCatching { + override fun waitForBackupUploadSteadyState(): Flow { + return callbackFlow { service.waitForBackupUploadSteadyState( progressListener = object : BackupSteadyStateListener { override fun onUpdate(status: RustBackupUploadState) { - backupUploadStateStateFlow.value = backupUploadStateMapper.map(status) + trySend(backupUploadStateMapper.map(status)) + if (status == RustBackupUploadState.Done) { + close() + } } } ) + awaitClose {} } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index 548b2f9165..825d929c0e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -22,14 +22,16 @@ import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf class FakeEncryptionService : EncryptionService { private var disableRecoveryFailure: Exception? = null override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) - override val backupUploadStateStateFlow: MutableStateFlow = MutableStateFlow(BackupUploadState.Unknown) + private var waitForBackupUploadSteadyStateFlow: Flow = flowOf() private var fixRecoveryIssuesFailure: Exception? = null @@ -73,12 +75,12 @@ class FakeEncryptionService : EncryptionService { return Result.success(Unit) } - override suspend fun waitForBackupUploadSteadyState(): Result { - return Result.success(Unit) + fun givenWaitForBackupUploadSteadyStateFlow(flow: Flow) { + waitForBackupUploadSteadyStateFlow = flow } - suspend fun emitBackupUploadState(state: BackupUploadState) { - backupUploadStateStateFlow.emit(state) + override fun waitForBackupUploadSteadyState(): Flow { + return waitForBackupUploadSteadyStateFlow } suspend fun emitBackupState(state: BackupState) { From 2d2088ce9a3e655dc8df9e0f0bcd48514d13c1b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 30 Oct 2023 16:48:59 +0100 Subject: [PATCH 254/281] Fix test. --- .../android/features/logout/impl/LogoutPresenterTest.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index 150401c902..fa02cdd68f 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -92,11 +92,13 @@ class LogoutPresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.isLastSession).isFalse() - assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Waiting) + assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.showConfirmationDialog).isFalse() assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) - val state = awaitItem() - assertThat(state.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) + val waitingState = awaitItem() + assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting) + val uploadingState = awaitItem() + assertThat(uploadingState.backupUploadState).isEqualTo(BackupUploadState.Uploading(backedUpCount = 1, totalCount = 2)) val doneState = awaitItem() assertThat(doneState.backupUploadState).isEqualTo(BackupUploadState.Done) } From 6258f341c98510885aef166094a208e5f3837fad Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 30 Oct 2023 19:31:26 +0000 Subject: [PATCH 255/281] Update screenshots --- ...gout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...gout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...l_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ll_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ll_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ll_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ll_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ull_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...cureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png | 4 ++-- 56 files changed, 112 insertions(+), 112 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png index 716325cb97..a876131abe 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8c36602ad1aa6a62cae8f69b4e0a185715a4105a958a17451e3f5573618a499 -size 28590 +oid sha256:75e44003e66f73983d63b0735217a3f99d1ef2af5778d53e256ab3c644c59cb8 +size 29114 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png index ba29e536fc..30b0828aa0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30777b75b1464bbaf0ab63c73189ae8d6b380d677ae87c22ad59a9052dbda564 -size 32530 +oid sha256:1a9b3f53a32a0924ae1db60f01df80e1e098d332ae29fdbfda527fb69b6a8860 +size 33097 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png index 48c0b62d21..41661a8a3c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d11018a66dcfca97713d7dfd5916a1c00f01881c21aad6014101a17b5b8912 -size 30167 +oid sha256:1807fd9e347c29c853d925c28f1e00259865eb9e7990fca1ad63e90a9eea8a1d +size 30676 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png index 6e7f76aa42..01bbb30c88 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b3b64d82224c1e28cfad0d5ac81796a2461311d63d23e84f9bf19b290b08bcd -size 37255 +oid sha256:0c140c29ad4a849893e08ed5b1943346ebc7ae5b888fb98b97088d2a5f025087 +size 37967 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png index 4bfb9042c4..c627fa8103 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aeee9a02b5bfa15ff7dc2e45f374237b2fa198df9e3a0a92598a950af7431a5b -size 34809 +oid sha256:d329bff55f22efa99e8afa20775a25f4c7e717cf5797ecc7a2c0f47e4cc1f69a +size 34937 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png index 292fdcf260..0e2172b118 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5437f1dd640de8500ce78fe810a92c5174879918b592e746f09a7244b4b98d74 -size 31993 +oid sha256:b72c4ecdb641d2adf53c092ac1ccc85018a747ffb0f4b911d3b10db6524ea897 +size 31588 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png index 7018ed9a82..9289d1049b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-D-0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3c05dd55dfd945a7c7528492d47698586089b618ae6ab811ae188a808a0a4b8 -size 33355 +oid sha256:704aac22e5063a3cc9b831c9079f1348385a1251c91630575200c33528fe3d76 +size 33480 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png index 71b951b61d..987b2930e6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39091e9262d0d57d51468ae809598d44829a2a33759e62f18154444c0c614f50 -size 26893 +oid sha256:ac0f94f62f6d89475fde477b095dc1242a606d9cd4bfa45a8cfaa8e368e22707 +size 27385 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png index bab7e54b27..c99598bb6a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:424a67c305485dc3d151d7771df9b9e724aa076e3758dc9cb341b86184163a39 -size 30551 +oid sha256:9f843cb5d7bceebbc6508257f0410af14242b23407ecea4643ab8a75329acca2 +size 30994 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png index 3dcfbdb383..7e0058627c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a1e679206fef656da4fbcba44220cc169d45e01faf74c6bdb174a807ccf89a2 -size 28529 +oid sha256:d9690fdef2aaabadb890c8649195de83cd60306f109346d292951ff6694037bd +size 28955 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png index 935078e5a2..b4f15d6aa3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f1a452799ce1f0055c17ec86a5dabe7d3f5f5688b15bc9e9f5cdde7c39774b7 -size 35541 +oid sha256:c75561844b2e5d163c7cdb49a36eb4f3adb4119fda2b9e4b6d9552daf447b907 +size 35817 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png index dc5cc2e1db..3bb60b709d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edf4ff593991f2cc88999034ff5ba80599d48ced6d23825a385ecf82d61d4686 -size 30383 +oid sha256:996388f47d4cd4c7c6318d335204a1b3610272ea1b340e4e36f10da222f3bcb6 +size 30476 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png index 9c73ebc664..21c4bbbf0d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63b528173b275d15fb8c0ec5d173b63636f24a3897227f4fb37e06a817a7521e -size 28881 +oid sha256:aa9f65a1f2e08e65f21aae2b4b6fb17b980262196443aec6b9cbc6c912077c78 +size 28497 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png index ee86ce04ea..f4e835079f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.logout.impl_null_LogoutView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5adffcc0b87707daba799293ec36b9e8202c72de2248835ed593b87fc82ea9b4 -size 29168 +oid sha256:9f63c93ba36566926aa471375f0bb341e1c989e5b1be9090f771597f91a7a226 +size 29258 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png index 1090a99a76..b9e83ad531 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b042f7e1248b739be1b0b96782d3970c0bfafa06c48ca507acbe882df471de3 -size 58735 +oid sha256:83e046cae9a59996697172a964c55c53a35ab22404b46cb2fdbf45956254d36d +size 59003 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png index d60a41a849..ffbaec9732 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6df1c5767b684843e39d31e6056133e72eb76d5d229db0d7176ff1be02852880 -size 48100 +oid sha256:576dc46e025bfa281a8f160315c7df052f8e9de85d1894a4a6b630eb10b4fad6 +size 47781 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png index 369174bf3a..eb6a8476b9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2c79522043f06f1a2d5810b1e2206d8b6d2ccea18389fb9a11a18887dc2d895 -size 59411 +oid sha256:4335f4f058d36b6cf9364ee2135fe692783c3a73302ad7267599ca0d62f20fd5 +size 59675 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png index 0941246116..7778573b62 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f5e1a5524453cc1814322315bde1180aa1e26f3f2bf22ec5e797c8767bd8675 -size 30634 +oid sha256:4ec952a7c1f8e032689bd045e7bf094987ef75e7ae63357ba247fbce087ff843 +size 30449 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png index ca51028a14..8250f88fc1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49f4d57ff2771fb32f198b506f0a0b7b7fd4f8f0af7f5b78c2369dfaec2e6543 -size 57060 +oid sha256:17747da89c21acc64c3b869d1aaea3ce6308139e8e505a973e6190cd2b48e6ed +size 57249 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png index d5e927ad65..7f195d3ca3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97c8f3ca9f5f9f4c89930b1279261125a548d5770a361f0f9749904212bb3fb3 -size 42305 +oid sha256:b5c652cb90b4e8d5849270ba732417c0dc9cbe5e1f84dcf6916dfad43f712dff +size 41926 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png index 2689301c41..dce81936c9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d34567f158c2480ba0c6884663b8c996d4754e069f558103b8de90a25a22a8e0 -size 57550 +oid sha256:5e68b8e510524f04f4f764e0fd33b2b0b3e395bd87eeae0e467007bf60ab2708 +size 57739 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png index d13b3cc27f..baa10cfe76 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.disable_null_SecureBackupDisableView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d50b5caf3f62d349985102f951a6bf679923b3726f420c6d55e128d6763d5b40 -size 27043 +oid sha256:4e3b46d43c8a9ce48c7226fc3159d1ec72fd55cbf7dd5b4de866fb4081e52116 +size 26804 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png index be946fad50..83407308e4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ffce711923870d9b43a9d4c2871ac1794d3d7e2b34a2063ab6f9e56c4e22dc0 -size 15203 +oid sha256:8a86b726999a4d994fa4f7a778e4f80b122e912c5c15285ea91fd8814d8eb743 +size 15694 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png index 95865f5b0d..77cc2a53d8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ef1cae71114c56ad6173cdeef356da56ce2769d5bd4a1332133f048d241a8d1 -size 15777 +oid sha256:5778ff8908d601c853a0eef84ce72026baa7ca461b8dc5b2c30e48c95153bc91 +size 16278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png index fb72a4d9b0..82499e566d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82d71934698b4e25afaaf9024c5dbb0571c3f8fd3270b5b07ff8d8dd9045bdd9 -size 21841 +oid sha256:e4b919c589d65d61c7ee11588ff628e02603cd3e4c5fc79a672d3991d64fcebd +size 22276 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png index 087096b93e..435255d5cf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4036acde06bd1fb9e256a97b53a6711ec45db4b67141b01231b491512608e457 -size 14271 +oid sha256:01653b0165eaf33b77948e41899cb21177fa70d9101eb04ad43ee7c7f4ab3607 +size 14718 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png index f8f8f17e86..91a649400a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d4d58607c126ae4a01a0ba62e09666fac11d3a2b05596742bc4e25cf75696fb -size 14830 +oid sha256:39d22d5c0e0b88869900ddf28de055188c17c3e59dfa528a2d81b552ebb140a3 +size 15282 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png index 4357469394..80e6693690 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enable_null_SecureBackupEnableView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e10389377cd21f189794e8a91cecee43b3c44a0030d5f312558dfef7add80dd1 -size 18548 +oid sha256:2d4856648cb21a768d174fe1ab6675be9c1c565e1d535cd3fe7d92d3f6c05d1c +size 18970 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png index c4852d22de..f3e629a2d4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90d7dab73ed32794eceac418541c2cf851d1e8bae0fdd5c34f8d9f0425890dc1 -size 33998 +oid sha256:05af89ec739d652391d64c56db2d5704075c34b8392386bfa6ca819b863b2fa0 +size 34283 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png index 87ad6fd5e8..3c9a0acc8f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9949ae01bf9aa195e88cb4d06ec0abd61e4bc3dee402f0cdf6617b7559ec318 -size 46006 +oid sha256:ad0763e5588212f988a56f1f2b3558c29b332ce832cb9ebaaffcbcebab4d457c +size 46477 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png index 74be4421e3..42a9ef2f9d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:386e41bb476411e1836be260ae309361b1493a914508484fda618783dffae00d -size 44521 +oid sha256:383a54396c54861554b53c95700ac49e84c0b9e0ded95aeb4246ab8aa7e9a703 +size 44834 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png index 96139ea253..0f69fa5cf5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9893ff64a23c98a13892235f36c52e2ab5ce5a57b3ee7953b884d81ebcfcd89 -size 33201 +oid sha256:f9892182ed80b39ebc3151335979967a516793b0ca761df35a7649091372d390 +size 32737 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png index 9122b9ec31..757c4ac571 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:691eb429d1e3a799d99213225a2bd92fa0b7b3ed8b384c8910ba114b5e05b514 -size 32221 +oid sha256:cad8f3834b84f1374fce154a90acc0b6d2279ecca29630575764962ef8a107e9 +size 32510 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png index 9a0a4ad046..d3b011ba6c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27e4d8474ca09ee1a76947464c1b8bd3c6aeb2f0b73b67cca5736d62d92e286d -size 42834 +oid sha256:9e421009e738803d834e4fe603900b828f5e4a2d85ffa5784e8d4f7a40810b80 +size 43156 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png index bb8bbedc22..ac630a70e9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:119fa492046cbfa51e9eb406fea4c4a9a6fb7dd152e3779153a2df2947800f76 -size 41383 +oid sha256:e254d17ebeb9e991eaeac000b9148a0c0279d31eac2996e8bf78491747089b30 +size 41767 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png index 1081be3ddc..caafa515ec 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_null_SecureBackupEnterRecoveryKeyView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dab45727bd828a76376b796e0d76794d7fb42ac80bc8c721b661eaf0926bb382 -size 29544 +oid sha256:b8ae3debf28076d7d0d71a2f3784c7d5cfd9dfc50ea534a4b44a952da1a4d947 +size 29166 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png index 1821bef52b..65ecaf38f8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2f9a83985ed5f607f6f6618794857468c4e71451bb58ba160478f8fb39a78a8 -size 51611 +oid sha256:09e3eb3b83d8f0b74e0d237b3424c68a9b90e077c37d2e472a194583086f7115 +size 51884 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png index ebae223f43..c3f68fac76 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cad87dcb5fb62c8d577c6e0ac4a13681a827c9d9bd1e2880d3cc3ad802802376 -size 49737 +oid sha256:a8cd8131baca93c8fb4c40cde91671155052cc749b0cefe4ba5a276f89f212ab +size 50017 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png index 92a65fe89d..a7fe29f246 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ac22cb35d4842fdab146bcaa8b89b6b09e53354c79fb6c865ab3b76b08c35a7 -size 53186 +oid sha256:744f0f6ec4910b54fd13ff423b1e0555ac0cff1c59123e6f5d096496f14603af +size 53238 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png index 92a65fe89d..a7fe29f246 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ac22cb35d4842fdab146bcaa8b89b6b09e53354c79fb6c865ab3b76b08c35a7 -size 53186 +oid sha256:744f0f6ec4910b54fd13ff423b1e0555ac0cff1c59123e6f5d096496f14603af +size 53238 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png index 9619617245..42f54c2b23 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-D-4_4_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14fd53641ce5e6d7246bbd575ba4d91d73a1ddda332fa134619afd011e9086bb -size 51267 +oid sha256:5d958c172b24915c6b03d8e75ca2611f95cf523eae30cdfb0af4bc40e27cb6c5 +size 48553 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png index 1bc25a1b2a..cb2fa2402e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:385b8e5012fad2197297fa1782226a0938a63e53bc4ed1dfa13d82b885a2ca74 -size 49691 +oid sha256:9973919b98ce55f98cb9bc00b98af8b90b7b7355235cad6bc1b4b8af799f6bc3 +size 49981 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png index c1cd47b777..c3f3f7c463 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e87bac6fa87595efb59245e2289ec7badeab60b959f7d99898cb404cf3c484f4 -size 47704 +oid sha256:ebc7011596661eafd90f646f59e986e7f46d942188fb0224fec03b8362284ab1 +size 47988 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png index 7800db3ce5..46fcda17a4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8f02343264623c0ee8d1eeb3ea48702f45d672bb4f319fd78e6eb488aab10a0 -size 50268 +oid sha256:5b4290983b3a2b2f13df38f6d95c2ae777230aae7018209f807793186eb2565b +size 50426 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png index 7800db3ce5..46fcda17a4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8f02343264623c0ee8d1eeb3ea48702f45d672bb4f319fd78e6eb488aab10a0 -size 50268 +oid sha256:5b4290983b3a2b2f13df38f6d95c2ae777230aae7018209f807793186eb2565b +size 50426 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png index 5ed4e0fa69..ab08a14180 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupView-N-4_5_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8be58c4edb9be3cadbd15ac98f8eabbf4ed72af5cac31f0d3d0a1f18c86c5046 -size 45221 +oid sha256:20530d92efe5c6c52daaa99342923ad7310f801515677899f77d1f5bf1de41f2 +size 42597 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png index 066d300929..23bb8e9846 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dba09f9888d8efd1036c66baf702d05bdd3f4a26c39ca14ae5c59ae2571d00b7 -size 50245 +oid sha256:dbe8d4ff5c2fca528c12e2475ec2179489c6cbaf3e98d0fa45f0980dcd4f9aab +size 50590 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png index 1fc4133905..1099362a48 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad43dd16b6b0232978b83795b739dcb71778a3a0faf7b8a4da599f23ec653237 -size 48125 +oid sha256:f1950689df7774679f0a10f679fc5be61917d3f89fe127dff037c119d675a0f7 +size 48482 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png index 92a65fe89d..a7fe29f246 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ac22cb35d4842fdab146bcaa8b89b6b09e53354c79fb6c865ab3b76b08c35a7 -size 53186 +oid sha256:744f0f6ec4910b54fd13ff423b1e0555ac0cff1c59123e6f5d096496f14603af +size 53238 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png index 92a65fe89d..a7fe29f246 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ac22cb35d4842fdab146bcaa8b89b6b09e53354c79fb6c865ab3b76b08c35a7 -size 53186 +oid sha256:744f0f6ec4910b54fd13ff423b1e0555ac0cff1c59123e6f5d096496f14603af +size 53238 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png index 9619617245..42f54c2b23 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-D-5_5_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14fd53641ce5e6d7246bbd575ba4d91d73a1ddda332fa134619afd011e9086bb -size 51267 +oid sha256:5d958c172b24915c6b03d8e75ca2611f95cf523eae30cdfb0af4bc40e27cb6c5 +size 48553 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png index 085a9a11e9..b84ddbac3c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67c874c41e3ce38f3cf2ccc6cf3759d6fec0a675fffa9cea94df341a8792c61e -size 48296 +oid sha256:1c426686054a0b72361c9ec2b93318c91f01f0d365d03593b03596dcfeef162d +size 48559 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png index d8fd472ea7..e6c46c2e47 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b32141d05f2e1b89eab149b91e02354b7db464a7c0b42516da74e18af777aa6 -size 46048 +oid sha256:1fca0245cec36a31642087fcafd4d65b1d670dfe315b6cadd0203be3e029984c +size 46307 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png index 7800db3ce5..46fcda17a4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8f02343264623c0ee8d1eeb3ea48702f45d672bb4f319fd78e6eb488aab10a0 -size 50268 +oid sha256:5b4290983b3a2b2f13df38f6d95c2ae777230aae7018209f807793186eb2565b +size 50426 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png index 7800db3ce5..46fcda17a4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8f02343264623c0ee8d1eeb3ea48702f45d672bb4f319fd78e6eb488aab10a0 -size 50268 +oid sha256:5b4290983b3a2b2f13df38f6d95c2ae777230aae7018209f807793186eb2565b +size 50426 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png index 5ed4e0fa69..ab08a14180 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.setup_null_SecureBackupSetupViewChange-N-5_6_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8be58c4edb9be3cadbd15ac98f8eabbf4ed72af5cac31f0d3d0a1f18c86c5046 -size 45221 +oid sha256:20530d92efe5c6c52daaa99342923ad7310f801515677899f77d1f5bf1de41f2 +size 42597 From 170524a605be56198e684fbe8c99fe2154f21097 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 22:55:11 +0000 Subject: [PATCH 256/281] Update coil to v2.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67d2e14d0f..5b8657fcdd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ accompanist = "0.32.0" test_core = "1.5.0" #other -coil = "2.4.0" +coil = "2.5.0" datetime = "0.4.1" serialization_json = "1.6.0" showkase = "1.0.0-beta18" From 5f857072351c8b922a292f1331ac812bc5325294 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 08:19:33 +0100 Subject: [PATCH 257/281] Update dependency io.gitlab.arturbosch.detekt to v1.23.2 (#1699) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67d2e14d0f..45d92b69b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,7 @@ anvil = "2.4.8-1-8" autoservice = "1.1.1" # quality -detekt = "1.23.1" +detekt = "1.23.2" dependencygraph = "0.12" junit = "4.13.2" androidx-test-ext-junit = "1.1.5" From 5757789d290d7419be5671b489f1a5547ac1e51c Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 31 Oct 2023 11:13:00 +0100 Subject: [PATCH 258/281] Add extra logs to help debug session restoration failures (#1701) --- .../kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt index 3e36e7d692..372fead0ee 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt @@ -92,7 +92,7 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient } .onFailure { - Timber.e("Fail to restore session") + Timber.e(it, "Fail to restore session") } } } From 95a215271461a8be39df2a6e2e2025668a5c9833 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Tue, 31 Oct 2023 13:00:08 +0000 Subject: [PATCH 259/281] Fix long press on voice message with screen reader (#1704) As a workaround, disable seeking within the waveform so that it does not interfere with the long press menu. Seeking behaviour is already suboptimal given that there is no spoken feedback about the current seek position. No core functionality is lost as voice messages can be played using a screen reader. --- .../components/event/TimelineItemVoiceView.kt | 4 ++ .../androidutils/accessibility/ContextExt.kt | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index 0c98321047..e7762fe278 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -42,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider +import io.element.android.libraries.androidutils.accessibility.isScreenReaderEnabled import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -86,6 +88,7 @@ fun TimelineItemVoiceView( overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.width(8.dp)) + val context = LocalContext.current WaveformPlaybackView( showCursor = state.button == VoiceMessageState.Button.Pause, playbackProgress = state.progress, @@ -93,6 +96,7 @@ fun TimelineItemVoiceView( modifier = Modifier .height(34.dp) .weight(1f), + seekEnabled = !context.isScreenReaderEnabled(), onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) }, ) Spacer(Modifier.width(extraPadding.getDpSize())) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt new file mode 100644 index 0000000000..bb223e43fb --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt @@ -0,0 +1,39 @@ +/* + * 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.androidutils.accessibility + +import android.content.Context +import android.view.accessibility.AccessibilityManager +import androidx.core.content.getSystemService + +/** + * Whether a screen reader is enabled. + * + * Avoid changing UI or app behavior based on the state of accessibility. + * See [AccessibilityManager.isTouchExplorationEnabled] for more details. + * + * @return true if the screen reader is enabled. + */ +fun Context.isScreenReaderEnabled(): Boolean { + val accessibilityManager = getSystemService() + ?: return false + + return accessibilityManager.let { + it.isEnabled && it.isTouchExplorationEnabled + } +} + From c377656354de3326d424a481b80fe358c2f3559f Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Tue, 31 Oct 2023 15:27:17 +0100 Subject: [PATCH 260/281] Show "Voice message" in voice message push notifications (#1705) Don't show the event body anymore as it's not relevant for voice messages. --- .../push/impl/notifications/NotifiableEventResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index c8d496a64e..82b076aa73 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 @@ -211,7 +211,7 @@ class NotifiableEventResolver @Inject constructor( ): String { return when (val messageType = content.messageType) { is AudioMessageType -> messageType.body - is VoiceMessageType -> messageType.body + is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message) is EmoteMessageType -> "* $senderDisplayName ${messageType.body}" is FileMessageType -> messageType.body is ImageMessageType -> messageType.body From 3bbaf8e5e7812d404fce5f1a6bd15f3b3f4956f2 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 31 Oct 2023 16:43:18 +0100 Subject: [PATCH 261/281] Improve the logs for `TimelineException.CannotPaginate` (#1708) --- .../libraries/matrix/impl/timeline/RustMatrixTimeline.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 1f0d13c9bf..74ad10ad26 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -194,8 +194,12 @@ class RustMatrixTimeline( waitForToken = true, ) innerRoom.paginateBackwards(paginationOptions) - }.onFailure { - Timber.d(it, "Fail to paginate for room ${matrixRoom.roomId}") + }.onFailure { error -> + if (error is TimelineException.CannotPaginate) { + Timber.d("Can't paginate backwards on room ${matrixRoom.roomId}, we're already at the start") + } else { + Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}") + } }.onSuccess { Timber.v("Success back paginating for room ${matrixRoom.roomId}") } From f554fb8dcc42482707db3a4af3eea7f2732f4875 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 31 Oct 2023 16:48:24 +0100 Subject: [PATCH 262/281] Change FeatureFlagService.isFeatureEnabled return value from Boolean to Flow --- .../featureflag/api/FeatureFlagService.kt | 12 +++++++++++- libraries/featureflag/impl/build.gradle.kts | 1 + .../impl/DefaultFeatureFlagService.kt | 8 +++++--- .../featureflag/impl/FeatureFlagProvider.kt | 3 ++- .../impl/PreferencesFeatureFlagProvider.kt | 7 ++++--- .../impl/StaticFeatureFlagProvider.kt | 9 ++++++--- .../impl/DefaultFeatureFlagServiceTest.kt | 19 +++++++++++++------ .../impl/FakeMutableFeatureFlagProvider.kt | 11 +++++++---- libraries/featureflag/test/build.gradle.kts | 1 + .../test/FakeFeatureFlagService.kt | 16 ++++++++++++---- 10 files changed, 62 insertions(+), 25 deletions(-) diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt index 8089c837ff..cd0c8937d7 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt @@ -16,13 +16,23 @@ package io.element.android.libraries.featureflag.api +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + interface FeatureFlagService { /** * @param feature the feature to check for * * @return true if the feature is enabled */ - suspend fun isFeatureEnabled(feature: Feature): Boolean + suspend fun isFeatureEnabled(feature: Feature): Boolean = isFeatureEnabledFlow(feature).first() + + /** + * @param feature the feature to check for + * + * @return a flow of booleans, true if the feature is enabled, false if it is disabled. + */ + fun isFeatureEnabledFlow(feature: Feature): Flow /** * @param feature the feature to enable or disable diff --git a/libraries/featureflag/impl/build.gradle.kts b/libraries/featureflag/impl/build.gradle.kts index ba0747b28e..64fcd1eece 100644 --- a/libraries/featureflag/impl/build.gradle.kts +++ b/libraries/featureflag/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(libs.androidx.datastore.preferences) implementation(projects.libraries.di) implementation(projects.libraries.core) + implementation(libs.coroutines.core) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.test.truth) diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt index b445599693..fd6f1b1f47 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagService.kt @@ -21,6 +21,8 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlagService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -29,12 +31,12 @@ class DefaultFeatureFlagService @Inject constructor( private val providers: Set<@JvmSuppressWildcards FeatureFlagProvider> ) : FeatureFlagService { - override suspend fun isFeatureEnabled(feature: Feature): Boolean { + override fun isFeatureEnabledFlow(feature: Feature): Flow { return providers.filter { it.hasFeature(feature) } .sortedByDescending(FeatureFlagProvider::priority) .firstOrNull() - ?.isFeatureEnabled(feature) - ?: feature.defaultValue + ?.isFeatureEnabledFlow(feature) + ?: flowOf(feature.defaultValue) } override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt index 833d9f9003..e40d59ec5b 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/FeatureFlagProvider.kt @@ -17,10 +17,11 @@ package io.element.android.libraries.featureflag.impl import io.element.android.libraries.featureflag.api.Feature +import kotlinx.coroutines.flow.Flow interface FeatureFlagProvider { val priority: Int - suspend fun isFeatureEnabled(feature: Feature): Boolean + fun isFeatureEnabledFlow(feature: Feature): Flow fun hasFeature(feature: Feature): Boolean } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index ddffdebd34..76344e078c 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -24,7 +24,8 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.featureflag.api.Feature -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -44,10 +45,10 @@ class PreferencesFeatureFlagProvider @Inject constructor(@ApplicationContext con } } - override suspend fun isFeatureEnabled(feature: Feature): Boolean { + override fun isFeatureEnabledFlow(feature: Feature): Flow { return store.data.map { prefs -> prefs[booleanPreferencesKey(feature.key)] ?: feature.defaultValue - }.first() + }.distinctUntilChanged() } override fun hasFeature(feature: Feature): Boolean { 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 82471c5983..e9dcc88a21 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 @@ -18,6 +18,8 @@ package io.element.android.libraries.featureflag.impl import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlags +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import javax.inject.Inject /** @@ -29,9 +31,9 @@ class StaticFeatureFlagProvider @Inject constructor() : override val priority = LOW_PRIORITY - override suspend fun isFeatureEnabled(feature: Feature): Boolean { - return if (feature is FeatureFlags) { - when(feature) { + override fun isFeatureEnabledFlow(feature: Feature): Flow { + val isFeatureEnabled = if (feature is FeatureFlags) { + when (feature) { FeatureFlags.LocationSharing -> true FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> true @@ -43,6 +45,7 @@ class StaticFeatureFlagProvider @Inject constructor() : } else { false } + return flowOf(isFeatureEnabled) } override fun hasFeature(feature: Feature) = true diff --git a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt index 890959639f..117005fd6f 100644 --- a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt +++ b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/DefaultFeatureFlagServiceTest.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.featureflag.impl +import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.api.FeatureFlags import kotlinx.coroutines.test.runTest @@ -26,8 +27,10 @@ class DefaultFeatureFlagServiceTest { @Test fun `given service without provider when feature is checked then it returns the default value`() = runTest { val featureFlagService = DefaultFeatureFlagService(emptySet()) - val isFeatureEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing) - assertThat(isFeatureEnabled).isEqualTo(FeatureFlags.LocationSharing.defaultValue) + featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test { + assertThat(awaitItem()).isEqualTo(FeatureFlags.LocationSharing.defaultValue) + cancelAndIgnoreRemainingEvents() + } } @Test @@ -50,9 +53,11 @@ class DefaultFeatureFlagServiceTest { val featureFlagProvider = FakeMutableFeatureFlagProvider(0) val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider)) featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true) - assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true) - featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, false) - assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(false) + featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test { + assertThat(awaitItem()).isEqualTo(true) + featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, false) + assertThat(awaitItem()).isEqualTo(false) + } } @Test @@ -62,6 +67,8 @@ class DefaultFeatureFlagServiceTest { val featureFlagService = DefaultFeatureFlagService(setOf(lowPriorityFeatureFlagProvider, highPriorityFeatureFlagProvider)) lowPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, false) highPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, true) - assertThat(featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)).isEqualTo(true) + featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test { + assertThat(awaitItem()).isEqualTo(true) + } } } diff --git a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt index f1d075ace2..20242cca69 100644 --- a/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/test/kotlin/io/element/android/libraries/featureflag/impl/FakeMutableFeatureFlagProvider.kt @@ -17,17 +17,20 @@ package io.element.android.libraries.featureflag.impl import io.element.android.libraries.featureflag.api.Feature +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow class FakeMutableFeatureFlagProvider(override val priority: Int) : MutableFeatureFlagProvider { - private val enabledFeatures = HashMap() + private val enabledFeatures = mutableMapOf>() override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean) { - enabledFeatures[feature.key] = enabled + val flow = enabledFeatures.getOrPut(feature.key) { MutableStateFlow(enabled) } + flow.emit(enabled) } - override suspend fun isFeatureEnabled(feature: Feature): Boolean { - return enabledFeatures[feature.key] ?: feature.defaultValue + override fun isFeatureEnabledFlow(feature: Feature): Flow { + return enabledFeatures.getOrPut(feature.key) { MutableStateFlow(feature.defaultValue) } } override fun hasFeature(feature: Feature): Boolean = true diff --git a/libraries/featureflag/test/build.gradle.kts b/libraries/featureflag/test/build.gradle.kts index 952b9323f6..3c3b199ce6 100644 --- a/libraries/featureflag/test/build.gradle.kts +++ b/libraries/featureflag/test/build.gradle.kts @@ -23,5 +23,6 @@ android { dependencies { api(projects.libraries.featureflag.api) + implementation(libs.coroutines.core) } } diff --git a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt index 9c86bad752..18c9920d74 100644 --- a/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt +++ b/libraries/featureflag/test/src/main/java/io/element/android/libraries/featureflag/test/FakeFeatureFlagService.kt @@ -18,19 +18,27 @@ package io.element.android.libraries.featureflag.test import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlagService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow class FakeFeatureFlagService( initialState: Map = emptyMap() ) : FeatureFlagService { - private val enabledFeatures = HashMap(initialState) + private val enabledFeatures = initialState + .map { + it.key to MutableStateFlow(it.value) + } + .toMap() + .toMutableMap() override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean { - enabledFeatures[feature.key] = enabled + val flow = enabledFeatures.getOrPut(feature.key) { MutableStateFlow(enabled) } + flow.emit(enabled) return true } - override suspend fun isFeatureEnabled(feature: Feature): Boolean { - return enabledFeatures[feature.key] ?: false + override fun isFeatureEnabledFlow(feature: Feature): Flow { + return enabledFeatures.getOrPut(feature.key) { MutableStateFlow(feature.defaultValue) } } } From b5c68f1b95b9574d13c522cf822aa18af0925a04 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 31 Oct 2023 13:14:04 +0100 Subject: [PATCH 263/281] Fix tests --- .../advanced/AdvancedSettingsPresenterTest.kt | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index ec49d57fb9..c2a7fb3e20 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -41,10 +42,10 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.isDeveloperModeEnabled).isFalse() assertThat(initialState.isRichTextEditorEnabled).isFalse() - assertThat(initialState.customElementCallBaseUrlState).isNull() + assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull() } } @@ -56,7 +57,7 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.isDeveloperModeEnabled).isFalse() initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) assertThat(awaitItem().isDeveloperModeEnabled).isTrue() @@ -73,7 +74,7 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.isRichTextEditorEnabled).isFalse() initialState.eventSink.invoke(AdvancedSettingsEvents.SetRichTextEditorEnabled(true)) assertThat(awaitItem().isRichTextEditorEnabled).isTrue() @@ -92,7 +93,7 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.customElementCallBaseUrlState).isNull() } } @@ -105,10 +106,7 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - // Initial state has a default `false` feature flag value, so the state will still be null - skipItems(1) - - val initialState = awaitItem() + val initialState = awaitLastSequentialItem() assertThat(initialState.customElementCallBaseUrlState).isNotNull() assertThat(initialState.customElementCallBaseUrlState?.baseUrl).isNull() @@ -128,10 +126,7 @@ class AdvancedSettingsPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - // Initial state has a default `false` feature flag value, so the state will still be null - skipItems(1) - - val urlValidator = awaitItem().customElementCallBaseUrlState!!.validator + val urlValidator = awaitLastSequentialItem().customElementCallBaseUrlState!!.validator assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one assertThat(urlValidator("test")).isFalse() assertThat(urlValidator("http://")).isFalse() From 355ee9596492233d018d27bebfae62082b67a571 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 31 Oct 2023 16:58:33 +0100 Subject: [PATCH 264/281] [Element Call] Keep MatrixClient alive while the call is working (#1695) * Element Call: keep MatrixClient alive to get event updates --- .../android/appnav/di/MatrixClientsHolder.kt | 2 +- ...CallScreeEvents.kt => CallScreenEvents.kt} | 6 +- .../features/call/ui/CallScreenPresenter.kt | 47 +++++++++++-- .../features/call/ui/CallScreenState.kt | 2 +- .../features/call/ui/CallScreenView.kt | 6 +- .../call/ui/CallScreenPresenterTest.kt | 69 +++++++++++++++++-- .../matrix/api/MatrixClientProvider.kt | 7 ++ .../matrix/test/FakeMatrixClientProvider.kt | 2 + 8 files changed, 125 insertions(+), 16 deletions(-) rename features/call/src/main/kotlin/io/element/android/features/call/ui/{CallScreeEvents.kt => CallScreenEvents.kt} (86%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt index 372fead0ee..f75725b1bb 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt @@ -49,7 +49,7 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: sessionIdsToMatrixClient.remove(sessionId) } - fun getOrNull(sessionId: SessionId): MatrixClient? { + override fun getOrNull(sessionId: SessionId): MatrixClient? { return sessionIdsToMatrixClient[sessionId] } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt similarity index 86% rename from features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt rename to features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt index 8ed4454fea..d16baacf3e 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreeEvents.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenEvents.kt @@ -18,7 +18,7 @@ package io.element.android.features.call.ui import io.element.android.features.call.utils.WidgetMessageInterceptor -sealed interface CallScreeEvents { - data object Hangup : CallScreeEvents - data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreeEvents +sealed interface CallScreenEvents { + data object Hangup : CallScreenEvents + data class SetupMessageChannels(val widgetMessageInterceptor: WidgetMessageInterceptor) : CallScreenEvents } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt index f9bb8bde2f..6883ebeb61 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenPresenter.kt @@ -17,6 +17,7 @@ package io.element.android.features.call.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -37,10 +38,13 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -53,9 +57,11 @@ class CallScreenPresenter @AssistedInject constructor( @Assisted private val callType: CallType, @Assisted private val navigator: CallScreenNavigator, private val callWidgetProvider: CallWidgetProvider, - private val userAgentProvider: UserAgentProvider, + userAgentProvider: UserAgentProvider, private val clock: SystemClock, private val dispatchers: CoroutineDispatchers, + private val matrixClientsProvider: MatrixClientProvider, + private val appCoroutineScope: CoroutineScope, ) : Presenter { @AssistedFactory @@ -78,6 +84,8 @@ class CallScreenPresenter @AssistedInject constructor( loadUrl(callType, urlState, callWidgetDriver) } + HandleMatrixClientSyncState() + callWidgetDriver.value?.let { driver -> LaunchedEffect(Unit) { driver.incomingMessages @@ -115,21 +123,22 @@ class CallScreenPresenter @AssistedInject constructor( } } - fun handleEvents(event: CallScreeEvents) { + fun handleEvents(event: CallScreenEvents) { when (event) { - is CallScreeEvents.Hangup -> { + is CallScreenEvents.Hangup -> { val widgetId = callWidgetDriver.value?.id val interceptor = messageInterceptor.value if (widgetId != null && interceptor != null && isJoinedCall) { // If the call was joined, we need to hang up first. Then the UI will be dismissed automatically. sendHangupMessage(widgetId, interceptor) + isJoinedCall = false } else { coroutineScope.launch { close(callWidgetDriver.value, navigator) } } } - is CallScreeEvents.SetupMessageChannels -> { + is CallScreenEvents.SetupMessageChannels -> { messageInterceptor.value = event.widgetMessageInterceptor } } @@ -166,6 +175,36 @@ class CallScreenPresenter @AssistedInject constructor( } } + @Composable + private fun HandleMatrixClientSyncState() { + val coroutineScope = rememberCoroutineScope() + DisposableEffect(Unit) { + val client = (callType as? CallType.RoomCall)?.sessionId?.let { + matrixClientsProvider.getOrNull(it) + } ?: return@DisposableEffect onDispose { } + + coroutineScope.launch { + client.syncService().syncState + .onEach { state -> + if (state != SyncState.Running) { + client.syncService().startSync() + } + } + .collect() + } + onDispose { + // We can't use the local coroutine scope here because it will be disposed before this effect + appCoroutineScope.launch { + client.syncService().run { + if (syncState.value == SyncState.Running) { + stopSync() + } + } + } + } + } + } + private fun parseMessage(message: String): WidgetMessage? { return WidgetMessageSerializer.deserialize(message).getOrNull() } diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt index d9716251fc..12cd7612ae 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenState.kt @@ -22,5 +22,5 @@ data class CallScreenState( val urlState: Async, val userAgent: String, val isInWidgetMode: Boolean, - val eventSink: (CallScreeEvents) -> Unit, + val eventSink: (CallScreenEvents) -> Unit, ) diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt index 33611515a7..acc01e6149 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/CallScreenView.kt @@ -65,14 +65,14 @@ internal fun CallScreenView( navigationIcon = { BackButton( resourceId = CommonDrawables.ic_compound_close, - onClick = { state.eventSink(CallScreeEvents.Hangup) } + onClick = { state.eventSink(CallScreenEvents.Hangup) } ) } ) } ) { padding -> BackHandler { - state.eventSink(CallScreeEvents.Hangup) + state.eventSink(CallScreenEvents.Hangup) } CallWebView( modifier = Modifier @@ -88,7 +88,7 @@ internal fun CallScreenView( }, onWebViewCreated = { webView -> val interceptor = WebViewWidgetMessageInterceptor(webView) - state.eventSink(CallScreeEvents.SetupMessageChannels(interceptor)) + state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) } ) } diff --git a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt index c318b1dfaa..77f83de209 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt @@ -25,14 +25,21 @@ import io.element.android.features.call.utils.FakeCallWidgetProvider import io.element.android.features.call.utils.FakeWidgetMessageInterceptor import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent @@ -95,7 +102,7 @@ class CallScreenPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) // And incoming message from the Widget Driver is passed to the WebView widgetDriver.givenIncomingMessage("A message") @@ -125,9 +132,9 @@ class CallScreenPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) - initialState.eventSink(CallScreeEvents.Hangup) + initialState.eventSink(CallScreenEvents.Hangup) // Let background coroutines run runCurrent() @@ -155,7 +162,7 @@ class CallScreenPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(CallScreeEvents.SetupMessageChannels(messageInterceptor)) + initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor)) messageInterceptor.givenInterceptedMessage("""{"action":"im.vector.hangup","api":"fromWidget","widgetId":"1","requestId":"1"}""") @@ -169,12 +176,64 @@ class CallScreenPresenterTest { } } + @Test + fun `present - automatically starts the Matrix client sync when on RoomCall`() = runTest { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeWidgetDriver() + val matrixClient = FakeMatrixClient() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilTimeout() + + assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - automatically stops the Matrix client sync on dispose`() = runTest { + val navigator = FakeCallScreenNavigator() + val widgetDriver = FakeWidgetDriver() + val matrixClient = FakeMatrixClient() + val presenter = createCallScreenPresenter( + callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), + widgetDriver = widgetDriver, + navigator = navigator, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(matrixClient) }) + ) + val hasRun = Mutex(true) + val job = launch { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.collect { + hasRun.unlock() + } + } + + hasRun.lock() + + job.cancelAndJoin() + + assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Terminated) + } + private fun TestScope.createCallScreenPresenter( callType: CallType, navigator: CallScreenNavigator = FakeCallScreenNavigator(), widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), widgetProvider: FakeCallWidgetProvider = FakeCallWidgetProvider(widgetDriver), dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), + matrixClientsProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), ): CallScreenPresenter { val userAgentProvider = object : UserAgentProvider { override fun provide(): String { @@ -189,6 +248,8 @@ class CallScreenPresenterTest { userAgentProvider, clock, dispatchers, + matrixClientsProvider, + this, ) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt index 44d1a1d1a6..eaa0356209 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt @@ -25,4 +25,11 @@ interface MatrixClientProvider { * Most of the time you want to use injected constructor instead of retrieving a MatrixClient with this provider. */ suspend fun getOrRestore(sessionId: SessionId): Result + + /** + * Can be used to retrieve an existing [MatrixClient] with the given [SessionId]. + * @param sessionId the [SessionId] of the [MatrixClient] to retrieve. + * @return the [MatrixClient] if it exists. + */ + fun getOrNull(sessionId: SessionId): MatrixClient? } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt index 80cdcff7ec..53ebf00f41 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClientProvider.kt @@ -24,4 +24,6 @@ class FakeMatrixClientProvider( private val getClient: (SessionId) -> Result = { Result.success(FakeMatrixClient()) } ) : MatrixClientProvider { override suspend fun getOrRestore(sessionId: SessionId): Result = getClient(sessionId) + + override fun getOrNull(sessionId: SessionId): MatrixClient? = getClient(sessionId).getOrNull() } From ed26c7462740cb4d110fb56059b3be8cba3645c9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 31 Oct 2023 18:23:48 +0100 Subject: [PATCH 265/281] LockScreen : enable the feature --- .../element/android/libraries/featureflag/api/FeatureFlags.kt | 2 +- .../libraries/featureflag/impl/StaticFeatureFlagProvider.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 f7dad97490..e04c1e6cd2 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 @@ -53,7 +53,7 @@ enum class FeatureFlags( key = "feature.pinunlock", title = "Pin unlock", description = "Allow user to lock/unlock the app with a pin code or biometrics", - defaultValue = false, + defaultValue = true, ), InRoomCalls( key = "feature.elementcall", 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 e9dcc88a21..3e67fef201 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 @@ -38,7 +38,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> true FeatureFlags.VoiceMessages -> true - FeatureFlags.PinUnlock -> false + FeatureFlags.PinUnlock -> true FeatureFlags.InRoomCalls -> true FeatureFlags.Mentions -> false } From 6832b1f2db972d1638a1cf55954ca2a1fd2346cc Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 31 Oct 2023 19:22:43 +0100 Subject: [PATCH 266/281] Feature/fga/biometric unlock (#1702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Biometric unlock : refactor a bit existing classes * Biometric unlock : first implementation * Biometric: add ui for biometric setup * Biometric unlock : use localazy strings * Biometric unlock setup : branch skip/allow events * Biometric : fix tests * Biometrics: add small test * Biometric : clean up * Update screenshots * Biometric unlock : address some PR review * Biometric : improve a bit edge cases * Fix lint issues --------- Co-authored-by: ganfra Co-authored-by: ElementBot Co-authored-by: Jorge Martín --- .../io/element/android/x/MainActivity.kt | 4 +- app/src/main/res/values-night/themes.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- .../android/appconfig/LockScreenConfig.kt | 17 +- features/lockscreen/impl/build.gradle.kts | 1 + .../impl/DefaultLockScreenService.kt | 20 ++- .../lockscreen/impl/LockScreenFlowNode.kt | 4 +- .../impl/biometric/BiometricUnlock.kt | 134 ++++++++++++++++ .../impl/biometric/BiometricUnlockError.kt | 38 +++++ .../impl/biometric/BiometricUnlockManager.kt | 38 +++++ .../DefaultBiometricUnlockCallback.kt | 14 +- .../DefaultBiometricUnlockManager.kt | 148 ++++++++++++++++++ .../impl/pin/DefaultPinCodeManager.kt | 32 ++-- .../impl/settings/LockScreenSettingsEvents.kt | 2 +- .../settings/LockScreenSettingsFlowNode.kt | 24 ++- .../settings/LockScreenSettingsPresenter.kt | 16 +- .../impl/settings/LockScreenSettingsState.kt | 1 + .../LockScreenSettingsStateProvider.kt | 2 + .../impl/settings/LockScreenSettingsView.kt | 12 +- .../impl/setup/LockScreenSetupFlowNode.kt | 115 ++++++++++++++ .../setup/biometric/SetupBiometricEvents.kt | 22 +++ .../setup/biometric/SetupBiometricNode.kt | 59 +++++++ .../biometric/SetupBiometricPresenter.kt | 61 ++++++++ .../setup/biometric/SetupBiometricState.kt | 22 +++ .../biometric/SetupBiometricStateProvider.kt | 33 ++++ .../setup/biometric/SetupBiometricView.kt | 105 +++++++++++++ .../impl/setup/{ => pin}/SetupPinEvents.kt | 2 +- .../impl/setup/{ => pin}/SetupPinNode.kt | 2 +- .../impl/setup/{ => pin}/SetupPinPresenter.kt | 6 +- .../impl/setup/{ => pin}/SetupPinState.kt | 4 +- .../setup/{ => pin}/SetupPinStateProvider.kt | 4 +- .../impl/setup/{ => pin}/SetupPinView.kt | 4 +- .../{ => pin}/validation/PinValidator.kt | 2 +- .../{ => pin}/validation/SetupPinFailure.kt | 2 +- .../storage/EncryptedPinCodeStorage.kt | 2 +- .../LockScreenStore.kt} | 18 ++- .../PreferencesLockScreenStore.kt} | 21 ++- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 1 + .../impl/unlock/PinUnlockPresenter.kt | 21 ++- .../lockscreen/impl/unlock/PinUnlockState.kt | 14 ++ .../impl/unlock/PinUnlockStateProvider.kt | 6 + .../lockscreen/impl/unlock/PinUnlockView.kt | 20 ++- .../impl/src/main/res/values/localazy.xml | 3 + .../biometric/FakeBiometricUnlockManager.kt | 41 +++++ .../impl/fixtures/LockScreenConfig.kt | 10 +- .../impl/fixtures/PinCodeManager.kt | 12 +- .../impl/pin/DefaultPinCodeManagerTest.kt | 10 +- ...odeStore.kt => InMemoryLockScreenStore.kt} | 15 +- .../LockScreenSettingsPresenterTest.kt | 7 +- .../biometric/SetupBiometricPresenterTest.kt | 74 +++++++++ .../setup/{ => pin}/SetupPinPresenterTest.kt | 6 +- .../impl/unlock/PinUnlockPresenterTest.kt | 14 +- gradle/libs.versions.toml | 3 +- .../cryptography/api/SecretKeyRepository.kt | 38 +++++ .../cryptography/impl/CryptographyModule.kt | 37 +++++ ...ider.kt => KeyStoreSecretKeyRepository.kt} | 26 +-- ...ovider.kt => SimpleSecretKeyRepository.kt} | 10 +- .../src/main/res/values/localazy.xml | 2 + ...etricView-D-2_2_null_0,NEXUS_5,1.0,en].png | 3 + ...etricView-N-2_3_null_0,NEXUS_5,1.0,en].png | 3 + ...pPinView-D-3_3_null_0,NEXUS_5,1.0,en].png} | 0 ...pPinView-D-3_3_null_1,NEXUS_5,1.0,en].png} | 0 ...pPinView-D-3_3_null_2,NEXUS_5,1.0,en].png} | 0 ...pPinView-D-3_3_null_3,NEXUS_5,1.0,en].png} | 0 ...pPinView-D-3_3_null_4,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_0,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_1,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_2,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_3,NEXUS_5,1.0,en].png} | 0 ...pPinView-N-3_4_null_4,NEXUS_5,1.0,en].png} | 0 ..._PinKeypad-D-5_5_null,NEXUS_5,1.0,en].png} | 0 ..._PinKeypad-N-5_6_null,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_0,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_1,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_2,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_3,NEXUS_5,1.0,en].png} | 0 ...nlockView-D-4_4_null_4,NEXUS_5,1.0,en].png | 3 + ...lockView-D-4_4_null_5,NEXUS_5,1.0,en].png} | 0 ...lockView-D-4_4_null_6,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_0,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_1,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_2,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_3,NEXUS_5,1.0,en].png} | 0 ...nlockView-N-4_5_null_4,NEXUS_5,1.0,en].png | 3 + ...lockView-N-4_5_null_5,NEXUS_5,1.0,en].png} | 0 ...lockView-N-4_5_null_6,NEXUS_5,1.0,en].png} | 0 86 files changed, 1270 insertions(+), 107 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlock.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockError.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/BiometricUnlockManager.kt rename libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyProvider.kt => features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockCallback.kt (66%) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/biometric/DefaultBiometricUnlockManager.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/LockScreenSetupFlowNode.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricEvents.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricNode.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenter.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricState.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinEvents.kt (92%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinNode.kt (96%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinPresenter.kt (94%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinState.kt (87%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinStateProvider.kt (93%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinView.kt (97%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/validation/PinValidator.kt (94%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/validation/SetupPinFailure.kt (90%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{pin => }/storage/EncryptedPinCodeStorage.kt (95%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{pin/storage/PinCodeStore.kt => storage/LockScreenStore.kt} (71%) rename features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/{pin/storage/PreferencesPinCodeStore.kt => storage/PreferencesLockScreenStore.kt} (82%) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/biometric/FakeBiometricUnlockManager.kt rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/{InMemoryPinCodeStore.kt => InMemoryLockScreenStore.kt} (73%) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricPresenterTest.kt rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/{ => pin}/SetupPinPresenterTest.kt (96%) create mode 100644 libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt create mode 100644 libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt rename libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/{KeyStoreSecretKeyProvider.kt => KeyStoreSecretKeyRepository.kt} (75%) rename libraries/cryptography/test/src/main/kotlin/io/element/android/libraries/cryptography/test/{SimpleSecretKeyProvider.kt => SimpleSecretKeyRepository.kt} (78%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-D-2_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup.biometric_null_SetupBiometricView-N-2_3_null_0,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-D-3_3_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_3,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.setup.pin_null_SetupPinView-N-3_4_null_4,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-5_5_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-5_6_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_4,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-4_4_null_6,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_4,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_5,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-4_5_null_6,NEXUS_5,1.0,en].png} (100%) 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 3b35a0ebf4..7109743052 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -30,7 +30,7 @@ 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.NodeComponentActivity +import com.bumble.appyx.core.integrationpoint.NodeActivity import com.bumble.appyx.core.plugin.NodeReadyObserver import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag @@ -42,7 +42,7 @@ import timber.log.Timber private val loggerTag = LoggerTag("MainActivity") -class MainActivity : NodeComponentActivity() { +class MainActivity : NodeActivity() { private lateinit var mainNode: MainNode diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index a6572451d7..fad45b6def 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -22,5 +22,5 @@ @style/Theme.ElementX - -