diff --git a/.gitattributes b/.gitattributes index 2062142284..d0cf036126 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ **/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text **/docs/images-lfs/*.png filter=lfs diff=lfs merge=lfs -text +libraries/mediaupload/impl/src/test/assets/* filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63ac00e289..a1df487c9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,12 +33,12 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - name: Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.9.0 + uses: gradle/gradle-build-action@v2.10.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 5ae475767e..a4bf0cc5ae 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -33,7 +33,7 @@ jobs: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 name: Use JDK 17 with: distribution: 'temurin' # See 'Supported distributions' for available options diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7f551cb981..4b0b1ff53c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 5562a67cec..d19e214f99 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -21,7 +21,7 @@ jobs: uses: nschloe/action-cached-lfs-checkout@v1.2.2 - name: Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' @@ -57,12 +57,12 @@ jobs: steps: - uses: actions/checkout@v4 - name: Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.9.0 + uses: gradle/gradle-build-action@v2.10.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index eba18e01c1..c9d2ea7d18 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -35,12 +35,12 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - name: Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.9.0 + uses: gradle/gradle-build-action@v2.10.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run code quality check suite diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 93cbbf83b5..8f72e4ec4a 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -33,13 +33,13 @@ jobs: with: persist-credentials: false - name: ☕️ Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' # Add gradle cache, this should speed up the process - name: Configure gradle - uses: gradle/gradle-build-action@v2.9.0 + uses: gradle/gradle-build-action@v2.10.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Record screenshots diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ebc6dd4e3..924d115046 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,12 +20,12 @@ jobs: steps: - uses: actions/checkout@v4 - name: Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.9.0 + uses: gradle/gradle-build-action@v2.10.0 - name: Create app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index e6fdbefa9e..d73a885220 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -27,12 +27,12 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - name: Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.9.0 + uses: gradle/gradle-build-action@v2.10.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: 🔊 Publish results to Sonar diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 87f46cb47a..49951cf350 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,12 +39,12 @@ jobs: # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - name: ☕️ Use JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.9.0 + uses: gradle/gradle-build-action@v2.10.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt index d412519d35..94cd7fa7e1 100644 --- a/app/src/main/kotlin/io/element/android/x/MainNode.kt +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -52,7 +52,7 @@ class MainNode( override val daggerComponent = (context as DaggerComponentOwner).daggerComponent override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node { - return createNode(context = buildContext) + return createNode(buildContext = buildContext) } @Composable 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 23c35b119a..80c1a06737 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -256,6 +256,10 @@ class LoggedInFlowNode @AssistedInject constructor( } is NavTarget.Room -> { val callback = object : RoomLoadedFlowNode.Callback { + override fun onOpenRoom(roomId: RoomId) { + backstack.push(NavTarget.Room(roomId)) + } + override fun onForwardedToSingleRoom(roomId: RoomId) { coroutineScope.launch { attachRoom(roomId) } } 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 613ed650c8..3cc405d7fd 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 @@ -74,6 +74,7 @@ class RoomLoadedFlowNode @AssistedInject constructor( ), DaggerComponentOwner { interface Callback : Plugin { + fun onOpenRoom(roomId: RoomId) fun onForwardedToSingleRoom(roomId: RoomId) fun onOpenGlobalNotificationSettings() } @@ -134,6 +135,10 @@ class RoomLoadedFlowNode @AssistedInject constructor( override fun onOpenGlobalNotificationSettings() { callbacks.forEach { it.onOpenGlobalNotificationSettings() } } + + override fun onOpenRoom(roomId: RoomId) { + callbacks.forEach { it.onOpenRoom(roomId) } + } } return roomDetailsEntryPoint.nodeBuilder(this, buildContext) .params(RoomDetailsEntryPoint.Params(initialTarget)) diff --git a/build.gradle.kts b/build.gradle.kts index a2dbec823c..7d06c2bf18 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -236,11 +236,11 @@ koverMerged { name = "Global minimum code coverage." target = kotlinx.kover.api.VerificationTarget.ALL bound { - minValue = 60 + minValue = 65 // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. // For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update // minValue to 25 and maxValue to 35. - maxValue = 70 + maxValue = 75 counter = kotlinx.kover.api.CounterType.INSTRUCTION valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE } @@ -376,3 +376,22 @@ subprojects { } } } + +subprojects { + tasks.withType().configureEach { + kotlinOptions { + if (project.findProperty("composeCompilerReports") == "true") { + freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler" + ) + } + if (project.findProperty("composeCompilerMetrics") == "true") { + freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler" + ) + } + } + } +} diff --git a/changelog.d/+make-matrix-classes-immutable.misc b/changelog.d/+make-matrix-classes-immutable.misc new file mode 100644 index 0000000000..0dcb7dedb9 --- /dev/null +++ b/changelog.d/+make-matrix-classes-immutable.misc @@ -0,0 +1 @@ +Make most code used in Compose from `:libraries:matrix` and derived classes Immutable or Stable. diff --git a/changelog.d/+set-default-power-level-to-join-calls.bugfix b/changelog.d/+set-default-power-level-to-join-calls.bugfix new file mode 100644 index 0000000000..fa143aa850 --- /dev/null +++ b/changelog.d/+set-default-power-level-to-join-calls.bugfix @@ -0,0 +1 @@ +Set a default power level to join calls. Also, create new rooms taking this power level into account. diff --git a/changelog.d/1451.feature b/changelog.d/1451.feature new file mode 100644 index 0000000000..1869f0ecd5 --- /dev/null +++ b/changelog.d/1451.feature @@ -0,0 +1 @@ +Display different notifications for mentions. diff --git a/changelog.d/1877.feature b/changelog.d/1877.feature new file mode 100644 index 0000000000..1156908c71 --- /dev/null +++ b/changelog.d/1877.feature @@ -0,0 +1 @@ +Scroll to end of timeline when sending a new message. diff --git a/changelog.d/1886.feature b/changelog.d/1886.feature new file mode 100644 index 0000000000..ac88b61b8c --- /dev/null +++ b/changelog.d/1886.feature @@ -0,0 +1 @@ +Confirm back navigation when editing a poll only if the poll was changed \ No newline at end of file diff --git a/changelog.d/1895.feature b/changelog.d/1895.feature new file mode 100644 index 0000000000..a683847fce --- /dev/null +++ b/changelog.d/1895.feature @@ -0,0 +1 @@ +Add option to delete a poll while editing the poll \ No newline at end of file diff --git a/changelog.d/1907.feature b/changelog.d/1907.feature new file mode 100644 index 0000000000..9ee2cba969 --- /dev/null +++ b/changelog.d/1907.feature @@ -0,0 +1 @@ +Open room member avatar when you click on it inside the member details screen. diff --git a/changelog.d/1912.bugfix b/changelog.d/1912.bugfix new file mode 100644 index 0000000000..63dbe0b496 --- /dev/null +++ b/changelog.d/1912.bugfix @@ -0,0 +1 @@ +Use the right avatar for DMs in DM rooms diff --git a/changelog.d/1920.misc b/changelog.d/1920.misc new file mode 100644 index 0000000000..4c59527599 --- /dev/null +++ b/changelog.d/1920.misc @@ -0,0 +1 @@ +RoomList: introduce incremental loading to improve performances. diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.kt new file mode 100644 index 0000000000..ef95f15dce --- /dev/null +++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/StartDMAction.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.createroom.api + +import androidx.compose.runtime.MutableState +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId + +interface StartDMAction { + /** + * Try to find an existing DM with the given user, or create one if none exists. + * @param userId The user to start a DM with. + * @param actionState The state to update with the result of the action. + */ + suspend fun execute(userId: UserId, actionState: MutableState>) +} diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index ae9818b727..6cd9b30741 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.usersearch.test) + testImplementation(projects.features.createroom.test) testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt index 3b96ac3edd..98ee988faa 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt @@ -77,11 +77,11 @@ class ConfigureRoomFlowNode @AssistedInject constructor( backstack.push(NavTarget.ConfigureRoom) } } - createNode(context = buildContext, plugins = listOf(callback)) + createNode(buildContext = buildContext, plugins = listOf(callback)) } NavTarget.ConfigureRoom -> { val callbacks = plugins() - createNode(context = buildContext, plugins = callbacks) + createNode(buildContext = buildContext, plugins = callbacks) } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 207ab73e66..7ba85f20c2 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -72,7 +72,7 @@ class CreateRoomFlowNode @AssistedInject constructor( plugins().forEach { it.onSuccess(roomId) } } } - createNode(context = buildContext, plugins = listOf(callback)) + createNode(buildContext = buildContext, plugins = listOf(callback)) } NavTarget.NewRoom -> { val callback = object : ConfigureRoomNode.Callback { @@ -80,7 +80,7 @@ class CreateRoomFlowNode @AssistedInject constructor( plugins().forEach { it.onSuccess(roomId) } } } - createNode(context = buildContext, plugins = listOf(callback)) + createNode(buildContext = buildContext, plugins = listOf(callback)) } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.kt new file mode 100644 index 0000000000..7145ac671e --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/DefaultStartDMAction.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.createroom.impl + +import androidx.compose.runtime.MutableState +import com.squareup.anvil.annotations.ContributesBinding +import im.vector.app.features.analytics.plan.CreatedRoom +import io.element.android.features.createroom.api.StartDMAction +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.StartDMResult +import io.element.android.libraries.matrix.api.room.startDM +import io.element.android.services.analytics.api.AnalyticsService +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultStartDMAction @Inject constructor( + private val matrixClient: MatrixClient, + private val analyticsService: AnalyticsService, +) : StartDMAction { + + override suspend fun execute(userId: UserId, actionState: MutableState>) { + actionState.value = Async.Loading() + when (val result = matrixClient.startDM(userId)) { + is StartDMResult.Success -> { + if (result.isNew) { + analyticsService.capture(CreatedRoom(isDM = true)) + } + actionState.value = Async.Success(result.roomId) + } + is StartDMResult.Failure -> { + actionState.value = Async.Failure(result.throwable) + } + } + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 839e4cd69e..9b60924ca4 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -21,21 +21,16 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import im.vector.app.features.analytics.plan.CreatedRoom +import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.createroom.impl.userlist.SelectionMode import io.element.android.features.createroom.impl.userlist.UserListDataStore import io.element.android.features.createroom.impl.userlist.UserListPresenter import io.element.android.features.createroom.impl.userlist.UserListPresenterArgs 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.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.usersearch.api.UserRepository -import io.element.android.services.analytics.api.AnalyticsService -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -43,8 +38,7 @@ class CreateRoomRootPresenter @Inject constructor( presenterFactory: UserListPresenter.Factory, userRepository: UserRepository, userListDataStore: UserListDataStore, - private val matrixClient: MatrixClient, - private val analyticsService: AnalyticsService, + private val startDMAction: StartDMAction, private val buildMeta: BuildMeta, ) : Presenter { @@ -61,37 +55,22 @@ class CreateRoomRootPresenter @Inject constructor( val userListState = presenter.present() val localCoroutineScope = rememberCoroutineScope() - val startDmAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val startDmActionState: MutableState> = remember { mutableStateOf(Async.Uninitialized) } fun handleEvents(event: CreateRoomRootEvents) { when (event) { - is CreateRoomRootEvents.StartDM -> localCoroutineScope.startDm(event.matrixUser, startDmAction) - CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized + is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch { + startDMAction.execute(event.matrixUser.userId, startDmActionState) + } + CreateRoomRootEvents.CancelStartDM -> startDmActionState.value = Async.Uninitialized } } return CreateRoomRootState( applicationName = buildMeta.applicationName, userListState = userListState, - startDmAction = startDmAction.value, + startDmAction = startDmActionState.value, eventSink = ::handleEvents, ) } - - private fun CoroutineScope.startDm(matrixUser: MatrixUser, startDmAction: MutableState>) = launch { - suspend { - matrixClient.findDM(matrixUser.userId).use { existingDM -> - existingDM?.roomId ?: createDM(matrixUser) - } - }.runCatchingUpdatingState(startDmAction) - } - - private suspend fun createDM(user: MatrixUser): RoomId { - return matrixClient - .createDM(user.userId) - .onSuccess { - analyticsService.capture(CreatedRoom(isDM = true)) - } - .getOrThrow() - } } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTests.kt new file mode 100644 index 0000000000..f2ed30c58d --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/DefaultStartDMActionTests.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl + +import androidx.compose.runtime.mutableStateOf +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.CreatedRoom +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +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 + +class DefaultStartDMActionTests { + + @Test + fun `when dm is found, assert state is updated with given room id`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenFindDmResult(A_ROOM_ID) + } + val action = createStartDMAction(matrixClient) + val state = mutableStateOf>(Async.Uninitialized) + action.execute(A_USER_ID, state) + assertThat(state.value).isEqualTo(Async.Success(A_ROOM_ID)) + } + + @Test + fun `when dm is not found, assert dm is created, state is updated with given room id and analytics get called`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenFindDmResult(null) + givenCreateDmResult(Result.success(A_ROOM_ID)) + } + val analyticsService = FakeAnalyticsService() + val action = createStartDMAction(matrixClient, analyticsService) + val state = mutableStateOf>(Async.Uninitialized) + action.execute(A_USER_ID, state) + assertThat(state.value).isEqualTo(Async.Success(A_ROOM_ID)) + assertThat(analyticsService.capturedEvents).containsExactly(CreatedRoom(isDM = true)) + } + + @Test + fun `when dm creation fails, assert state is updated with given error`() = runTest { + val matrixClient = FakeMatrixClient().apply { + givenFindDmResult(null) + givenCreateDmResult(Result.failure(A_THROWABLE)) + } + val action = createStartDMAction(matrixClient) + val state = mutableStateOf>(Async.Uninitialized) + action.execute(A_USER_ID, state) + assertThat(state.value).isEqualTo(Async.Failure(A_THROWABLE)) + } + + private fun createStartDMAction( + matrixClient: MatrixClient = FakeMatrixClient(), + analyticsService: AnalyticsService = FakeAnalyticsService(), + ): DefaultStartDMAction { + return DefaultStartDMAction( + matrixClient = matrixClient, + analyticsService = analyticsService, + ) + } +} diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index bf4593bdbb..a0fb71dd31 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -20,25 +20,21 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import im.vector.app.features.analytics.plan.CreatedRoom +import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.createroom.impl.userlist.FakeUserListPresenter import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory import io.element.android.features.createroom.impl.userlist.UserListDataStore -import io.element.android.features.createroom.impl.userlist.aUserListState +import io.element.android.features.createroom.test.FakeStartDMAction import io.element.android.libraries.architecture.Async 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.user.MatrixUser +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.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.usersearch.test.FakeUserRepository -import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Rule import org.junit.Test @@ -47,142 +43,57 @@ class CreateRoomRootPresenterTests { @get:Rule val warmUpRule = WarmUpRule() - private lateinit var userRepository: FakeUserRepository - private lateinit var presenter: CreateRoomRootPresenter - private lateinit var fakeUserListPresenter: FakeUserListPresenter - private lateinit var fakeMatrixClient: FakeMatrixClient - private lateinit var fakeAnalyticsService: FakeAnalyticsService - - @Before - fun setup() { - fakeUserListPresenter = FakeUserListPresenter() - fakeMatrixClient = FakeMatrixClient() - fakeAnalyticsService = FakeAnalyticsService() - userRepository = FakeUserRepository() - presenter = CreateRoomRootPresenter( - presenterFactory = FakeUserListPresenterFactory(fakeUserListPresenter), - userRepository = userRepository, - userListDataStore = UserListDataStore(), - matrixClient = fakeMatrixClient, - analyticsService = fakeAnalyticsService, - buildMeta = aBuildMeta(), - ) - } - @Test - fun `present - initial state`() = runTest { + fun `present - start DM action complete scenario`() = runTest { + val startDMAction = FakeStartDMAction() + val presenter = createCreateRoomRootPresenter(startDMAction) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() + assertThat(initialState.startDmAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(initialState.applicationName).isEqualTo(aBuildMeta().applicationName) assertThat(initialState.userListState.selectedUsers).isEmpty() assertThat(initialState.userListState.isSearchActive).isFalse() assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse() - } - } - @Test - fun `present - trigger create DM action`() = runTest { - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() val matrixUser = MatrixUser(UserId("@name:domain")) - val createDmResult = Result.success(RoomId("!createDmResult:domain")) - - fakeMatrixClient.givenFindDmResult(null) - fakeMatrixClient.givenCreateDmResult(createDmResult) - - initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) - assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) - val stateAfterStartDM = awaitItem() - assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java) - assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) - } - } - - @Test - fun `present - creating a DM records analytics event`() = runTest { - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val matrixUser = MatrixUser(UserId("@name:domain")) - val createDmResult = Result.success(RoomId("!createDmResult:domain")) - - fakeMatrixClient.givenFindDmResult(null) - fakeMatrixClient.givenCreateDmResult(createDmResult) - - initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) - skipItems(2) - - val analyticsEvent = fakeAnalyticsService.capturedEvents.filterIsInstance().firstOrNull() - assertThat(analyticsEvent).isNotNull() - assertThat(analyticsEvent?.isDM).isTrue() - } - } - - @Test - fun `present - trigger retrieve DM action`() = runTest { - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val matrixUser = MatrixUser(UserId("@name:domain")) - val fakeDmResult = FakeMatrixRoom(roomId = RoomId("!fakeDmResult:domain")) - - fakeMatrixClient.givenFindDmResult(fakeDmResult) - - initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) - val stateAfterStartDM = awaitItem() - assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Success::class.java) - assertThat(stateAfterStartDM.startDmAction.dataOrNull()).isEqualTo(fakeDmResult.roomId) - assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance()).isEmpty() - } - } - - @Test - fun `present - trigger retry create DM action`() = runTest { - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - val matrixUser = MatrixUser(UserId("@name:domain")) - val createDmResult = Result.success(RoomId("!createDmResult:domain")) - fakeUserListPresenter.givenState(aUserListState().copy(selectedUsers = persistentListOf(matrixUser))) - - fakeMatrixClient.givenFindDmResult(null) - fakeMatrixClient.givenCreateDmError(A_THROWABLE) - fakeMatrixClient.givenCreateDmResult(createDmResult) + val startDMSuccessResult = Async.Success(A_ROOM_ID) + val startDMFailureResult = Async.Failure(A_THROWABLE) // Failure + startDMAction.givenExecuteResult(startDMFailureResult) initialState.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) - val stateAfterStartDM = awaitItem() - assertThat(stateAfterStartDM.startDmAction).isInstanceOf(Async.Failure::class.java) - assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance()).isEmpty() + awaitItem().also { state -> + assertThat(state.startDmAction).isEqualTo(startDMFailureResult) + state.eventSink(CreateRoomRootEvents.CancelStartDM) + } - // Cancel - stateAfterStartDM.eventSink(CreateRoomRootEvents.CancelStartDM) - val stateAfterCancel = awaitItem() - assertThat(stateAfterCancel.startDmAction).isInstanceOf(Async.Uninitialized::class.java) - - // Failure - stateAfterCancel.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + // Success + startDMAction.givenExecuteResult(startDMSuccessResult) + awaitItem().also { state -> + assertThat(state.startDmAction).isEqualTo(Async.Uninitialized) + state.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) + } assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) - val stateAfterSecondAttempt = awaitItem() - assertThat(stateAfterSecondAttempt.startDmAction).isInstanceOf(Async.Failure::class.java) - assertThat(fakeAnalyticsService.capturedEvents.filterIsInstance()).isEmpty() + awaitItem().also { state -> + assertThat(state.startDmAction).isEqualTo(startDMSuccessResult) + } - // Retry with success - fakeMatrixClient.givenCreateDmError(null) - stateAfterSecondAttempt.eventSink(CreateRoomRootEvents.StartDM(matrixUser)) - assertThat(awaitItem().startDmAction).isInstanceOf(Async.Loading::class.java) - val stateAfterRetryStartDM = awaitItem() - assertThat(stateAfterRetryStartDM.startDmAction).isInstanceOf(Async.Success::class.java) - assertThat(stateAfterRetryStartDM.startDmAction.dataOrNull()).isEqualTo(createDmResult.getOrNull()) } } + + private fun createCreateRoomRootPresenter( + startDMAction: StartDMAction = FakeStartDMAction(), + ): CreateRoomRootPresenter { + return CreateRoomRootPresenter( + presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()), + userRepository = FakeUserRepository(), + userListDataStore = UserListDataStore(), + startDMAction = startDMAction, + buildMeta = aBuildMeta(), + ) + } } diff --git a/features/createroom/test/build.gradle.kts b/features/createroom/test/build.gradle.kts new file mode 100644 index 0000000000..53c3e6461a --- /dev/null +++ b/features/createroom/test/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * 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") +} + +android { + namespace = "io.element.android.features.createroom.test" +} + +dependencies { + implementation(libs.coroutines.core) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrix.test) + implementation(projects.libraries.architecture) + api(projects.features.createroom.api) +} diff --git a/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt b/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.kt new file mode 100644 index 0000000000..c1888dc53f --- /dev/null +++ b/features/createroom/test/src/main/kotlin/io/element/android/features/createroom/test/FakeStartDMAction.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.createroom.test + +import androidx.compose.runtime.MutableState +import io.element.android.features.createroom.api.StartDMAction +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.A_ROOM_ID +import kotlinx.coroutines.delay + +class FakeStartDMAction : StartDMAction { + + private var executeResult: Async = Async.Success(A_ROOM_ID) + + fun givenExecuteResult(result: Async) { + executeResult = result + } + + override suspend fun execute(userId: UserId, actionState: MutableState>) { + actionState.value = Async.Loading() + delay(1) + actionState.value = executeResult + } +} 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 65b6e91682..fd559642a5 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 @@ -140,8 +140,7 @@ private fun BackupUploadState.isBackingUp(): Boolean { return when (this) { BackupUploadState.Unknown, BackupUploadState.Waiting, - is BackupUploadState.Uploading, - is BackupUploadState.CheckingIfUploadNeeded -> true + is BackupUploadState.Uploading -> true is BackupUploadState.SteadyException -> exception is SteadyStateException.Connection BackupUploadState.Done, BackupUploadState.Error -> false diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 6faf07f916..e392c96e04 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -23,6 +23,11 @@ plugins { android { namespace = "io.element.android.features.messages.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -48,6 +53,7 @@ dependencies { implementation(projects.libraries.dateformatter.api) implementation(projects.libraries.eventformatter.api) implementation(projects.libraries.mediapickers.api) + implementation(projects.libraries.mediaviewer.api) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) @@ -87,7 +93,10 @@ dependencies { testImplementation(projects.libraries.textcomposer.test) testImplementation(projects.libraries.voicerecorder.test) testImplementation(projects.libraries.mediaplayer.test) + testImplementation(projects.libraries.mediaviewer.test) testImplementation(libs.test.mockk) + testImplementation(libs.test.junitext) + testImplementation(libs.test.robolectric) ksp(libs.showkase.processor) } 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 6176e5b598..7957eb2200 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 @@ -39,8 +39,6 @@ import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode import io.element.android.features.messages.impl.forward.ForwardMessagesNode -import io.element.android.features.messages.impl.media.local.MediaInfo -import io.element.android.features.messages.impl.media.viewer.MediaViewerNode import io.element.android.features.messages.impl.report.ReportMessageNode import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -62,6 +60,8 @@ 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.media.MediaSource import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode import kotlinx.collections.immutable.ImmutableList import kotlinx.parcelize.Parcelize @@ -180,6 +180,8 @@ class MessagesFlowNode @AssistedInject constructor( mediaInfo = navTarget.mediaInfo, mediaSource = navTarget.mediaSource, thumbnailSource = navTarget.thumbnailSource, + canDownload = true, + canShare = true, ) createNode(buildContext, listOf(inputs)) } 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 4496469c44..a765a395a3 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 @@ -79,6 +79,7 @@ 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.api.user.CurrentSessionIdHolder 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 @@ -108,6 +109,7 @@ class MessagesPresenter @AssistedInject constructor( private val featureFlagsService: FeatureFlagService, @Assisted private val navigator: MessagesNavigator, private val buildMeta: BuildMeta, + private val currentSessionIdHolder: CurrentSessionIdHolder, ) : Presenter { private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) @@ -144,6 +146,16 @@ class MessagesPresenter @AssistedInject constructor( mutableStateOf(false) } + var canJoinCall by rememberSaveable { + mutableStateOf(false) + } + + LaunchedEffect(currentSessionIdHolder.current) { + withContext(dispatchers.io) { + canJoinCall = room.canUserJoinCall(userId = currentSessionIdHolder.current).getOrDefault(false) + } + } + val inviteProgress = remember { mutableStateOf>(Async.Uninitialized) } var showReinvitePrompt by remember { mutableStateOf(false) } LaunchedEffect(hasDismissedInviteDialog, composerState.hasFocus, syncUpdateFlow) { @@ -162,8 +174,6 @@ class MessagesPresenter @AssistedInject constructor( val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) var enableVoiceMessages by remember { mutableStateOf(false) } - // TODO add min power level to use this feature in the future? - val enableInRoomCalls = true LaunchedEffect(featureFlagsService) { enableVoiceMessages = featureFlagsService.isFeatureEnabled(FeatureFlags.VoiceMessages) } @@ -193,6 +203,12 @@ class MessagesPresenter @AssistedInject constructor( } } + val callState = when { + !canJoinCall -> RoomCallState.DISABLED + roomInfo?.hasRoomCall == true -> RoomCallState.ONGOING + else -> RoomCallState.ENABLED + } + return MessagesState( roomId = room.roomId, roomName = roomName, @@ -213,9 +229,8 @@ class MessagesPresenter @AssistedInject constructor( inviteProgress = inviteProgress.value, enableTextFormatting = enableTextFormatting, enableVoiceMessages = enableVoiceMessages, - enableInRoomCalls = enableInRoomCalls, appName = buildMeta.applicationName, - isCallOngoing = roomInfo?.hasRoomCall ?: false, + callState = callState, eventSink = { handleEvents(it) } ) } @@ -224,7 +239,7 @@ class MessagesPresenter @AssistedInject constructor( return AvatarData( id = id, name = name, - url = avatarUrl, + url = avatarUrl ?: room.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 325c695988..7d342ab107 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 @@ -51,8 +51,13 @@ data class MessagesState( val showReinvitePrompt: Boolean, val enableTextFormatting: Boolean, val enableVoiceMessages: Boolean, - val enableInRoomCalls: Boolean, - val isCallOngoing: Boolean, + val callState: RoomCallState, val appName: String, val eventSink: (MessagesEvents) -> Unit ) + +enum class RoomCallState { + ENABLED, + ONGOING, + DISABLED +} 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 720c385f89..00c15fb410 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 @@ -66,7 +66,7 @@ open class MessagesStateProvider : PreviewParameterProvider { ), ), aMessagesState().copy( - isCallOngoing = true, + callState = RoomCallState.ONGOING, ), aMessagesState().copy( enableVoiceMessages = true, @@ -75,6 +75,9 @@ open class MessagesStateProvider : PreviewParameterProvider { showSendFailureDialog = true ), ), + aMessagesState().copy( + callState = RoomCallState.DISABLED, + ), ) } @@ -117,8 +120,7 @@ fun aMessagesState() = MessagesState( showReinvitePrompt = false, enableTextFormatting = true, enableVoiceMessages = true, - enableInRoomCalls = true, - isCallOngoing = false, + callState = RoomCallState.ENABLED, 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 d7e4bfedc9..3a26079a3d 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 @@ -191,10 +191,9 @@ fun MessagesView( MessagesViewTopBar( roomName = state.roomName.dataOrNull(), roomAvatar = state.roomAvatar.dataOrNull(), - inRoomCallsEnabled = state.enableInRoomCalls, + callState = state.callState, onBackPressed = onBackPressed, onRoomDetailsClicked = onRoomDetailsClicked, - isCallOngoing = state.isCallOngoing, onJoinCallClicked = onJoinCallClicked, ) } @@ -449,8 +448,7 @@ private fun MessagesViewComposerBottomSheetContents( private fun MessagesViewTopBar( roomName: String?, roomAvatar: AvatarData?, - inRoomCallsEnabled: Boolean, - isCallOngoing: Boolean, + callState: RoomCallState, onRoomDetailsClicked: () -> Unit, onJoinCallClicked: () -> Unit, onBackPressed: () -> Unit, @@ -477,13 +475,11 @@ private fun MessagesViewTopBar( } }, actions = { - if (inRoomCallsEnabled) { - if (isCallOngoing) { - JoinCallMenuItem(onJoinCallClicked = onJoinCallClicked) - } else { - IconButton(onClick = onJoinCallClicked) { - Icon(CompoundIcons.VideoCall, contentDescription = stringResource(CommonStrings.a11y_start_call)) - } + if (callState == RoomCallState.ONGOING) { + JoinCallMenuItem(onJoinCallClicked = onJoinCallClicked) + } else { + IconButton(onClick = onJoinCallClicked, enabled = callState != RoomCallState.DISABLED) { + Icon(CompoundIcons.VideoCall, contentDescription = stringResource(CommonStrings.a11y_start_call)) } } Spacer(Modifier.width(8.dp)) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt index 8739a45201..55c8033636 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt @@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.attachments import android.os.Parcelable import androidx.compose.runtime.Immutable -import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMedia import kotlinx.parcelize.Parcelize @Immutable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 983d016854..28d403c9a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import timber.log.Timber import kotlin.coroutines.coroutineContext class AttachmentsPreviewPresenter @AssistedInject constructor( @@ -114,6 +115,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( sendActionState.value = SendActionState.Done }, onFailure = { error -> + Timber.e(error, "Failed to send attachment") if (error is CancellationException) { throw error } else { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index de7f5cd47b..37b2ee9c78 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -19,10 +19,10 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.net.toUri import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.MediaInfo -import io.element.android.features.messages.impl.media.local.aFileInfo -import io.element.android.features.messages.impl.media.local.anImageInfo +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.local.aFileInfo +import io.element.android.libraries.mediaviewer.api.local.anImageInfo open class AttachmentsPreviewStateProvider : PreviewParameterProvider { override val values: Sequence diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 1bb8428e02..2e10eab9b6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp 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.LocalMediaView import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialogType @@ -40,6 +39,7 @@ import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.mediaviewer.api.local.LocalMediaView import io.element.android.libraries.ui.strings.CommonStrings @Composable 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 dd2537ff18..7a5c06721c 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 @@ -34,7 +34,6 @@ import androidx.media3.common.util.UnstableApi 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.MentionSuggestion import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor import io.element.android.libraries.architecture.Presenter @@ -51,6 +50,7 @@ import io.element.android.libraries.matrix.api.room.Mention 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.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.textcomposer.model.Message @@ -72,6 +72,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.merge import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.coroutineContext import kotlin.time.Duration.Companion.seconds @@ -432,6 +433,7 @@ class MessageComposerPresenter @Inject constructor( attachmentState.value = AttachmentsState.None } .onFailure { cause -> + Timber.e(cause, "Failed to send attachment") attachmentState.value = AttachmentsState.None if (cause is CancellationException) { throw cause diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 6deb6e1839..a2e9858d9f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -34,6 +34,7 @@ import im.vector.app.features.analytics.plan.PollEnd import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory +import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager @@ -98,7 +99,7 @@ class TimelinePresenter @AssistedInject constructor( val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } - val hasNewItems = remember { mutableStateOf(false) } + val newItemState = remember { mutableStateOf(NewEventState.None) } val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState() val keyBackupState by encryptionService.backupStateStateFlow.collectAsState() @@ -121,7 +122,7 @@ class TimelinePresenter @AssistedInject constructor( is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId is TimelineEvents.OnScrollFinished -> { if (event.firstIndex == 0) { - hasNewItems.value = false + newItemState.value = NewEventState.None } appScope.sendReadReceiptIfNeeded( firstVisibleIndex = event.firstIndex, @@ -150,7 +151,7 @@ class TimelinePresenter @AssistedInject constructor( } LaunchedEffect(timelineItems.size) { - computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems) + computeNewItemState(timelineItems, prevMostRecentItemId, newItemState) } LaunchedEffect(Unit) { @@ -182,7 +183,7 @@ class TimelinePresenter @AssistedInject constructor( paginationState = paginationState, timelineItems = timelineItems, showReadReceipts = readReceiptsEnabled, - hasNewItems = hasNewItems.value, + newEventState = newItemState.value, sessionState = sessionState, eventSink = ::handleEvents ) @@ -191,22 +192,32 @@ class TimelinePresenter @AssistedInject constructor( /** * This method compute the hasNewItem state passed as a [MutableState] each time the timeline items size changes. * Basically, if we got new timeline event from sync or local, either from us or another user, we update the state so we tell we have new items. - * The state never goes back to false from this method, but need to be reset from somewhere else. + * The state never goes back to None from this method, but need to be reset from somewhere else. */ - private suspend fun computeHasNewItems( + private suspend fun computeNewItemState( timelineItems: ImmutableList, prevMostRecentItemId: MutableState, - hasNewItemsState: MutableState + newEventState: MutableState ) = withContext(dispatchers.computation) { + // FromMe is prioritized over FromOther, so skip if we already have a FromMe + if (newEventState.value == NewEventState.FromMe) { + return@withContext + } val newMostRecentItem = timelineItems.firstOrNull() val prevMostRecentItemIdValue = prevMostRecentItemId.value val newMostRecentItemId = newMostRecentItem?.identifier() - val hasNewItems = prevMostRecentItemIdValue != null && + val hasNewEvent = prevMostRecentItemIdValue != null && newMostRecentItem is TimelineItem.Event && newMostRecentItem.origin != TimelineItemEventOrigin.PAGINATION && newMostRecentItemId != prevMostRecentItemIdValue - if (hasNewItems) { - hasNewItemsState.value = true + if (hasNewEvent) { + val newMostRecentEvent = newMostRecentItem as? TimelineItem.Event + val fromMe = newMostRecentEvent?.localSendState != null + newEventState.value = if (fromMe) { + NewEventState.FromMe + } else { + NewEventState.FromOther + } } prevMostRecentItemId.value = newMostRecentItemId } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 62836db130..36fa6d33d2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.libraries.matrix.api.core.EventId @@ -30,7 +31,7 @@ data class TimelineState( val highlightedEventId: EventId?, val userHasPermissionToSendMessage: Boolean, val paginationState: MatrixTimeline.PaginationState, - val hasNewItems: Boolean, + val newEventState: NewEventState, val sessionState: SessionState, val eventSink: (TimelineEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 852c931186..8616ecbc8f 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 @@ -16,7 +16,9 @@ package io.element.android.features.messages.impl.timeline +import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData import io.element.android.features.messages.impl.timeline.model.InReplyToDetails +import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition @@ -53,7 +55,7 @@ fun aTimelineState(timelineItems: ImmutableList = persistentListOf ), highlightedEventId = null, userHasPermissionToSendMessage = true, - hasNewItems = false, + newEventState = NewEventState.None, sessionState = aSessionState( isSessionVerified = true, isKeyBackupEnabled = true, @@ -186,17 +188,27 @@ internal fun aTimelineItemReadReceipts(): TimelineItemReadReceipts { ) } -fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents { - val event = aTimelineItemEvent( +internal fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents { + val event1 = aTimelineItemEvent( isMine = true, content = aTimelineItemStateEventContent(), - groupPosition = TimelineItemGroupPosition.None + groupPosition = TimelineItemGroupPosition.None, + readReceiptState = TimelineItemReadReceipts( + receipts = listOf(aReadReceiptData(0)).toPersistentList(), + ), ) + val event2 = aTimelineItemEvent( + isMine = true, + content = aTimelineItemStateEventContent(body = "Another state event"), + groupPosition = TimelineItemGroupPosition.None, + readReceiptState = TimelineItemReadReceipts( + receipts = listOf(aReadReceiptData(1)).toPersistentList(), + ), + ) + val events = listOf(event1, event2) return TimelineItem.GroupedEvents( id = id.toString(), - events = listOf( - event, - event, - ).toImmutableList() + events = events.toImmutableList(), + aggregatedReadReceipts = events.flatMap { it.readReceiptState.receipts }.toImmutableList(), ) } 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 99274a6b7b..44f8fce876 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 @@ -20,13 +20,11 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.tween import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut 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.fillMaxSize import androidx.compose.foundation.layout.padding @@ -42,36 +40,27 @@ import androidx.compose.runtime.CompositionLocalProvider 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.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.rotate import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.messages.impl.R -import io.element.android.features.messages.impl.timeline.components.TimelineItemEventRow -import io.element.android.features.messages.impl.timeline.components.TimelineItemStateEventRow -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.TimelineItemRow import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent -import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo -import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.libraries.designsystem.animation.alphaAnimation import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -167,141 +156,44 @@ fun TimelineView( TimelineScrollHelper( isTimelineEmpty = state.timelineItems.isEmpty(), lazyListState = lazyListState, - hasNewItems = state.hasNewItems, + newEventState = state.newEventState, onScrollFinishedAt = ::onScrollFinishedAt ) } } -@Composable -private fun TimelineItemRow( - timelineItem: TimelineItem, - showReadReceipts: Boolean, - isLastOutgoingMessage: Boolean, - highlightedItem: String?, - userHasPermissionToSendMessage: Boolean, - sessionState: SessionState, - onUserDataClick: (UserId) -> Unit, - onClick: (TimelineItem.Event) -> Unit, - onLongClick: (TimelineItem.Event) -> Unit, - inReplyToClick: (EventId) -> Unit, - onReactionClick: (key: String, TimelineItem.Event) -> Unit, - onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, - onMoreReactionsClick: (TimelineItem.Event) -> Unit, - onReadReceiptClick: (TimelineItem.Event) -> Unit, - onTimestampClicked: (TimelineItem.Event) -> Unit, - onSwipeToReply: (TimelineItem.Event) -> Unit, - eventSink: (TimelineEvents) -> Unit, - modifier: Modifier = Modifier -) { - when (timelineItem) { - is TimelineItem.Virtual -> { - TimelineItemVirtualRow( - virtual = timelineItem, - sessionState = sessionState, - modifier = modifier, - ) - } - is TimelineItem.Event -> { - if (timelineItem.content is TimelineItemStateContent) { - TimelineItemStateEventRow( - event = timelineItem, - isHighlighted = highlightedItem == timelineItem.identifier(), - onClick = { onClick(timelineItem) }, - onLongClick = { onLongClick(timelineItem) }, - eventSink = eventSink, - modifier = modifier, - ) - } else { - TimelineItemEventRow( - event = timelineItem, - showReadReceipts = showReadReceipts, - isLastOutgoingMessage = isLastOutgoingMessage, - isHighlighted = highlightedItem == timelineItem.identifier(), - canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(), - onClick = { onClick(timelineItem) }, - onLongClick = { onLongClick(timelineItem) }, - onUserDataClick = onUserDataClick, - inReplyToClick = inReplyToClick, - onReactionClick = onReactionClick, - onReactionLongClick = onReactionLongClick, - onMoreReactionsClick = onMoreReactionsClick, - onReadReceiptClick = onReadReceiptClick, - onTimestampClicked = onTimestampClicked, - onSwipeToReply = { onSwipeToReply(timelineItem) }, - eventSink = eventSink, - modifier = modifier, - ) - } - } - is TimelineItem.GroupedEvents -> { - val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) } - - fun onExpandGroupClick() { - isExpanded.value = !isExpanded.value - } - - Column(modifier = modifier.animateContentSize()) { - GroupHeaderView( - text = pluralStringResource( - id = R.plurals.room_timeline_state_changes, - count = timelineItem.events.size, - timelineItem.events.size - ), - isExpanded = isExpanded.value, - isHighlighted = !isExpanded.value && timelineItem.events.any { it.identifier() == highlightedItem }, - onClick = ::onExpandGroupClick, - ) - if (isExpanded.value) { - Column { - timelineItem.events.forEach { subGroupEvent -> - TimelineItemRow( - timelineItem = subGroupEvent, - showReadReceipts = showReadReceipts, - isLastOutgoingMessage = isLastOutgoingMessage, - highlightedItem = highlightedItem, - sessionState = sessionState, - userHasPermissionToSendMessage = false, - onClick = onClick, - onLongClick = onLongClick, - inReplyToClick = inReplyToClick, - onUserDataClick = onUserDataClick, - onTimestampClicked = onTimestampClicked, - onReactionClick = onReactionClick, - onReactionLongClick = onReactionLongClick, - onMoreReactionsClick = onMoreReactionsClick, - onReadReceiptClick = onReadReceiptClick, - eventSink = eventSink, - onSwipeToReply = {}, - ) - } - } - } - } - } - } -} - @Composable private fun BoxScope.TimelineScrollHelper( isTimelineEmpty: Boolean, lazyListState: LazyListState, - hasNewItems: Boolean, + newEventState: NewEventState, onScrollFinishedAt: (Int) -> Unit, ) { val coroutineScope = rememberCoroutineScope() val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } } - val canAutoScroll by remember { derivedStateOf { lazyListState.firstVisibleItemIndex < 3 } } + val canAutoScroll by remember { + derivedStateOf { + lazyListState.firstVisibleItemIndex < 3 + } + } - LaunchedEffect(canAutoScroll, hasNewItems) { - val shouldAutoScroll = isScrollFinished && canAutoScroll && hasNewItems - if (shouldAutoScroll) { - coroutineScope.launch { + fun scrollToBottom() { + coroutineScope.launch { + if (lazyListState.firstVisibleItemIndex > 10) { + lazyListState.scrollToItem(0) + } else { lazyListState.animateScrollToItem(0) } } } + LaunchedEffect(canAutoScroll, newEventState) { + val shouldAutoScroll = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe) + if (shouldAutoScroll) { + scrollToBottom() + } + } + LaunchedEffect(isScrollFinished, isTimelineEmpty) { if (isScrollFinished && !isTimelineEmpty) { // Notify the parent composable about the first visible item index when scrolling finishes @@ -315,15 +207,7 @@ private fun BoxScope.TimelineScrollHelper( modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 24.dp, bottom = 12.dp), - onClick = { - coroutineScope.launch { - if (lazyListState.firstVisibleItemIndex > 10) { - lazyListState.scrollToItem(0) - } else { - lazyListState.animateScrollToItem(0) - } - } - } + onClick = ::scrollToBottom, ) } 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 17582aa905..99fb5d3fb7 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 @@ -59,6 +59,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.constraintlayout.compose.ConstrainScope import androidx.constraintlayout.compose.ConstraintLayout +import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView @@ -66,6 +67,7 @@ import io.element.android.features.messages.impl.timeline.components.event.toExt import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView import io.element.android.features.messages.impl.timeline.model.InReplyToDetails +import io.element.android.features.messages.impl.timeline.model.InReplyToMetadata import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState @@ -75,6 +77,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.timeline.model.metadata import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -89,18 +92,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType -import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType -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.MessageContent -import io.element.android.libraries.matrix.api.timeline.item.event.PollContent -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 -import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch import kotlin.math.abs @@ -171,8 +163,6 @@ fun TimelineItemEventRow( state = state.draggableState, ), event = event, - showReadReceipts = showReadReceipts, - isLastOutgoingMessage = isLastOutgoingMessage, isHighlighted = isHighlighted, interactionSource = interactionSource, onClick = onClick, @@ -183,7 +173,6 @@ fun TimelineItemEventRow( onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) }, - onReadReceiptsClicked = { onReadReceiptClick(event) }, eventSink = eventSink, ) } @@ -191,8 +180,6 @@ fun TimelineItemEventRow( } else { TimelineItemEventRowContent( event = event, - showReadReceipts = showReadReceipts, - isLastOutgoingMessage = isLastOutgoingMessage, isHighlighted = isHighlighted, interactionSource = interactionSource, onClick = onClick, @@ -203,10 +190,20 @@ fun TimelineItemEventRow( onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) }, - onReadReceiptsClicked = { onReadReceiptClick(event) }, eventSink = eventSink, ) } + // Read receipts / Send state + TimelineItemReadReceiptView( + state = ReadReceiptViewState( + sendState = event.localSendState, + isLastOutgoingMessage = isLastOutgoingMessage, + receipts = event.readReceiptState.receipts, + ), + showReadReceipts = showReadReceipts, + onReadReceiptsClicked = { onReadReceiptClick(event) }, + modifier = Modifier.padding(top = 4.dp), + ) } } @@ -236,8 +233,6 @@ private fun SwipeSensitivity( @Composable private fun TimelineItemEventRowContent( event: TimelineItem.Event, - showReadReceipts: Boolean, - isLastOutgoingMessage: Boolean, isHighlighted: Boolean, interactionSource: MutableInteractionSource, onClick: () -> Unit, @@ -246,7 +241,6 @@ private fun TimelineItemEventRowContent( inReplyToClicked: () -> Unit, onUserDataClicked: () -> Unit, onReactionClicked: (emoji: String) -> Unit, - onReadReceiptsClicked: () -> Unit, onReactionLongClicked: (emoji: String) -> Unit, onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit, eventSink: (TimelineEvents) -> Unit, @@ -267,7 +261,6 @@ private fun TimelineItemEventRowContent( sender, message, reactions, - readReceipts, ) = createRefs() // Sender @@ -334,25 +327,6 @@ private fun TimelineItemEventRowContent( .padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp) ) } - - // Read receipts / Send state - TimelineItemReadReceiptView( - state = ReadReceiptViewState( - sendState = event.localSendState, - isLastOutgoingMessage = isLastOutgoingMessage, - receipts = event.readReceiptState.receipts, - ), - showReadReceipts = showReadReceipts, - onReadReceiptsClicked = onReadReceiptsClicked, - modifier = Modifier - .constrainAs(readReceipts) { - if (event.reactionsState.reactions.isNotEmpty()) { - top.linkTo(reactions.bottom, margin = 4.dp) - } else { - top.linkTo(message.bottom, margin = 4.dp) - } - } - ) } } @@ -538,13 +512,10 @@ private fun MessageEventBubbleContent( } val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> val senderName = inReplyTo.senderDisplayName ?: inReplyTo.senderId.value - val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyTo) - val text = textForInReplyTo(inReplyTo) val topPadding = if (showThreadDecoration) 0.dp else 8.dp ReplyToContent( senderName = senderName, - text = text, - attachmentThumbnailInfo = attachmentThumbnailInfo, + metadata = inReplyTo.metadata(), modifier = Modifier .padding(top = topPadding, start = 8.dp, end = 8.dp) .clip(RoundedCornerShape(6.dp)) @@ -585,11 +556,10 @@ private fun MessageEventBubbleContent( @Composable private fun ReplyToContent( senderName: String, - text: String?, - attachmentThumbnailInfo: AttachmentThumbnailInfo?, + metadata: InReplyToMetadata?, modifier: Modifier = Modifier, ) { - val paddings = if (attachmentThumbnailInfo != null) { + val paddings = if (metadata is InReplyToMetadata.Thumbnail) { PaddingValues(start = 4.dp, end = 12.dp, top = 4.dp, bottom = 4.dp) } else { PaddingValues(horizontal = 12.dp, vertical = 4.dp) @@ -599,9 +569,9 @@ private fun ReplyToContent( .background(MaterialTheme.colorScheme.surface) .padding(paddings) ) { - if (attachmentThumbnailInfo != null) { + if (metadata is InReplyToMetadata.Thumbnail) { AttachmentThumbnail( - info = attachmentThumbnailInfo, + info = metadata.attachmentThumbnailInfo, backgroundColor = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier .size(36.dp) @@ -619,7 +589,7 @@ private fun ReplyToContent( overflow = TextOverflow.Ellipsis, ) Text( - text = text.orEmpty(), + text = metadata?.text.orEmpty(), style = ElementTheme.typography.fontBodyMdRegular, textAlign = TextAlign.Start, color = ElementTheme.materialColors.secondary, @@ -630,60 +600,6 @@ private fun ReplyToContent( } } -private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyToDetails): AttachmentThumbnailInfo? { - return when (val eventContent = inReplyTo.eventContent) { - is MessageContent -> when (val type = eventContent.type) { - is ImageMessageType -> AttachmentThumbnailInfo( - thumbnailSource = type.info?.thumbnailSource ?: type.source, - textContent = eventContent.body, - type = AttachmentThumbnailType.Image, - blurHash = type.info?.blurhash, - ) - is VideoMessageType -> AttachmentThumbnailInfo( - thumbnailSource = type.info?.thumbnailSource, - textContent = eventContent.body, - type = AttachmentThumbnailType.Video, - blurHash = type.info?.blurhash, - ) - is FileMessageType -> AttachmentThumbnailInfo( - thumbnailSource = type.info?.thumbnailSource, - textContent = eventContent.body, - type = AttachmentThumbnailType.File, - ) - is LocationMessageType -> AttachmentThumbnailInfo( - textContent = eventContent.body, - type = AttachmentThumbnailType.Location, - ) - is AudioMessageType -> AttachmentThumbnailInfo( - textContent = eventContent.body, - type = AttachmentThumbnailType.Audio, - ) - is VoiceMessageType -> AttachmentThumbnailInfo( - type = AttachmentThumbnailType.Voice, - ) - else -> null - } - is PollContent -> AttachmentThumbnailInfo( - textContent = eventContent.question, - type = AttachmentThumbnailType.Poll, - ) - else -> null - } -} - -@Composable -private fun textForInReplyTo(inReplyTo: InReplyToDetails): String { - return when (val eventContent = inReplyTo.eventContent) { - is MessageContent -> when (eventContent.type) { - is LocationMessageType -> stringResource(CommonStrings.common_shared_location) - is VoiceMessageType -> stringResource(CommonStrings.common_voice_message) - else -> inReplyTo.textContent ?: eventContent.body - } - is PollContent -> eventContent.question - else -> "" - } -} - @PreviewsDayNight @Composable internal fun TimelineItemEventRowPreview() = ElementPreview { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt index f6b417e82d..3d03e99600 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt @@ -45,6 +45,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.PollContent 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 kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf @PreviewsDayNight @Composable @@ -127,9 +129,9 @@ class InReplyToDetailsProvider : PreviewParameterProvider { question = "Poll which are being replied.", kind = PollKind.Disclosed, maxSelections = 1u, - answers = emptyList(), - votes = emptyMap(), - endTime = null, + answers = persistentListOf(), + votes = persistentMapOf(), + endTime = null isEdited = false, ), ).map { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt new file mode 100644 index 0000000000..b09581016e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.timeline.TimelineEvents +import io.element.android.features.messages.impl.timeline.aGroupedEvents +import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView +import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState +import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.session.SessionState +import io.element.android.features.messages.impl.timeline.session.aSessionState +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId + +@Composable +fun TimelineItemGroupedEventsRow( + timelineItem: TimelineItem.GroupedEvents, + showReadReceipts: Boolean, + isLastOutgoingMessage: Boolean, + highlightedItem: String?, + sessionState: SessionState, + onClick: (TimelineItem.Event) -> Unit, + onLongClick: (TimelineItem.Event) -> Unit, + inReplyToClick: (EventId) -> Unit, + onUserDataClick: (UserId) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, + onReactionClick: (key: String, TimelineItem.Event) -> Unit, + onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, + onMoreReactionsClick: (TimelineItem.Event) -> Unit, + onReadReceiptClick: (TimelineItem.Event) -> Unit, + eventSink: (TimelineEvents) -> Unit, + modifier: Modifier = Modifier +) { + val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) } + + fun onExpandGroupClick() { + isExpanded.value = !isExpanded.value + } + + TimelineItemGroupedEventsRowContent( + isExpanded = isExpanded.value, + onExpandGroupClick = ::onExpandGroupClick, + timelineItem = timelineItem, + highlightedItem = highlightedItem, + showReadReceipts = showReadReceipts, + isLastOutgoingMessage = isLastOutgoingMessage, + sessionState = sessionState, + onClick = onClick, + onLongClick = onLongClick, + inReplyToClick = inReplyToClick, + onUserDataClick = onUserDataClick, + onTimestampClicked = onTimestampClicked, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + eventSink = eventSink, + modifier = modifier, + ) +} + +@Composable +private fun TimelineItemGroupedEventsRowContent( + isExpanded: Boolean, + onExpandGroupClick: () -> Unit, + timelineItem: TimelineItem.GroupedEvents, + highlightedItem: String?, + showReadReceipts: Boolean, + isLastOutgoingMessage: Boolean, + sessionState: SessionState, + onClick: (TimelineItem.Event) -> Unit, + onLongClick: (TimelineItem.Event) -> Unit, + inReplyToClick: (EventId) -> Unit, + onUserDataClick: (UserId) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, + onReactionClick: (key: String, TimelineItem.Event) -> Unit, + onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, + onMoreReactionsClick: (TimelineItem.Event) -> Unit, + onReadReceiptClick: (TimelineItem.Event) -> Unit, + eventSink: (TimelineEvents) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.animateContentSize()) { + GroupHeaderView( + text = pluralStringResource( + id = R.plurals.room_timeline_state_changes, + count = timelineItem.events.size, + timelineItem.events.size + ), + isExpanded = isExpanded, + isHighlighted = !isExpanded && timelineItem.events.any { it.identifier() == highlightedItem }, + onClick = onExpandGroupClick, + ) + if (isExpanded) { + Column { + timelineItem.events.forEach { subGroupEvent -> + TimelineItemRow( + timelineItem = subGroupEvent, + showReadReceipts = showReadReceipts, + isLastOutgoingMessage = isLastOutgoingMessage, + highlightedItem = highlightedItem, + sessionState = sessionState, + userHasPermissionToSendMessage = false, + onClick = onClick, + onLongClick = onLongClick, + inReplyToClick = inReplyToClick, + onUserDataClick = onUserDataClick, + onTimestampClicked = onTimestampClicked, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + eventSink = eventSink, + onSwipeToReply = {}, + ) + } + } + } else if (showReadReceipts) { + TimelineItemReadReceiptView( + state = ReadReceiptViewState( + sendState = null, + isLastOutgoingMessage = false, + receipts = timelineItem.aggregatedReadReceipts, + ), + showReadReceipts = true, + onReadReceiptsClicked = onExpandGroupClick + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPreview { + TimelineItemGroupedEventsRowContent( + isExpanded = true, + onExpandGroupClick = {}, + timelineItem = aGroupedEvents(), + highlightedItem = null, + showReadReceipts = true, + isLastOutgoingMessage = false, + sessionState = aSessionState(), + onClick = {}, + onLongClick = {}, + inReplyToClick = {}, + onUserDataClick = {}, + onTimestampClicked = {}, + onReactionClick = { _, _ -> }, + onReactionLongClick = { _, _ -> }, + onMoreReactionsClick = {}, + onReadReceiptClick = {}, + eventSink = {}, + ) +} + +@PreviewsDayNight +@Composable +internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPreview { + TimelineItemGroupedEventsRowContent( + isExpanded = false, + onExpandGroupClick = {}, + timelineItem = aGroupedEvents(), + highlightedItem = null, + showReadReceipts = true, + isLastOutgoingMessage = false, + sessionState = aSessionState(), + onClick = {}, + onLongClick = {}, + inReplyToClick = {}, + onUserDataClick = {}, + onTimestampClicked = {}, + onReactionClick = { _, _ -> }, + onReactionLongClick = { _, _ -> }, + onMoreReactionsClick = {}, + onReadReceiptClick = {}, + eventSink = {}, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt new file mode 100644 index 0000000000..f77319259b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.element.android.features.messages.impl.timeline.TimelineEvents +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo +import io.element.android.features.messages.impl.timeline.session.SessionState +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId + +@Composable +internal fun TimelineItemRow( + timelineItem: TimelineItem, + showReadReceipts: Boolean, + isLastOutgoingMessage: Boolean, + highlightedItem: String?, + userHasPermissionToSendMessage: Boolean, + sessionState: SessionState, + onUserDataClick: (UserId) -> Unit, + onClick: (TimelineItem.Event) -> Unit, + onLongClick: (TimelineItem.Event) -> Unit, + inReplyToClick: (EventId) -> Unit, + onReactionClick: (key: String, TimelineItem.Event) -> Unit, + onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, + onMoreReactionsClick: (TimelineItem.Event) -> Unit, + onReadReceiptClick: (TimelineItem.Event) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, + onSwipeToReply: (TimelineItem.Event) -> Unit, + eventSink: (TimelineEvents) -> Unit, + modifier: Modifier = Modifier +) { + when (timelineItem) { + is TimelineItem.Virtual -> { + TimelineItemVirtualRow( + virtual = timelineItem, + sessionState = sessionState, + modifier = modifier, + ) + } + is TimelineItem.Event -> { + if (timelineItem.content is TimelineItemStateContent) { + TimelineItemStateEventRow( + event = timelineItem, + showReadReceipts = showReadReceipts, + isLastOutgoingMessage = isLastOutgoingMessage, + isHighlighted = highlightedItem == timelineItem.identifier(), + onClick = { onClick(timelineItem) }, + onReadReceiptsClick = onReadReceiptClick, + onLongClick = { onLongClick(timelineItem) }, + eventSink = eventSink, + modifier = modifier, + ) + } else { + TimelineItemEventRow( + event = timelineItem, + showReadReceipts = showReadReceipts, + isLastOutgoingMessage = isLastOutgoingMessage, + isHighlighted = highlightedItem == timelineItem.identifier(), + canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(), + onClick = { onClick(timelineItem) }, + onLongClick = { onLongClick(timelineItem) }, + onUserDataClick = onUserDataClick, + inReplyToClick = inReplyToClick, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + onTimestampClicked = onTimestampClicked, + onSwipeToReply = { onSwipeToReply(timelineItem) }, + eventSink = eventSink, + modifier = modifier, + ) + } + } + is TimelineItem.GroupedEvents -> { + TimelineItemGroupedEventsRow( + timelineItem = timelineItem, + showReadReceipts = showReadReceipts, + isLastOutgoingMessage = isLastOutgoingMessage, + highlightedItem = highlightedItem, + sessionState = sessionState, + onClick = onClick, + onLongClick = onLongClick, + inReplyToClick = inReplyToClick, + onUserDataClick = onUserDataClick, + onTimestampClicked = onTimestampClicked, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + eventSink = eventSink, + modifier = modifier, + ) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index 4d75994c5f..fbf39125b3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn @@ -32,51 +33,73 @@ import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding +import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState +import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView +import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition +import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import kotlinx.collections.immutable.toPersistentList @Composable fun TimelineItemStateEventRow( event: TimelineItem.Event, + showReadReceipts: Boolean, + isLastOutgoingMessage: Boolean, isHighlighted: Boolean, onClick: () -> Unit, onLongClick: () -> Unit, + onReadReceiptsClick: (event: TimelineItem.Event) -> Unit, eventSink: (TimelineEvents) -> Unit, modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } - Box( + Column( modifier = modifier .fillMaxWidth() - .padding(vertical = 8.dp) - .wrapContentHeight(), - contentAlignment = Alignment.Center ) { - MessageStateEventContainer( - isHighlighted = isHighlighted, - interactionSource = interactionSource, - onClick = onClick, - onLongClick = onLongClick, + Box( modifier = Modifier - .zIndex(-1f) - .widthIn(max = 320.dp) + .fillMaxWidth() + .padding(top = 8.dp, bottom = 2.dp) + .wrapContentHeight(), + contentAlignment = Alignment.Center ) { - TimelineItemEventContentView( - content = event.content, - isMine = event.isMine, - isEditable = event.isEditable, + MessageStateEventContainer( + isHighlighted = isHighlighted, interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, - extraPadding = noExtraPadding, - eventSink = eventSink, - modifier = Modifier.defaultTimelineContentPadding() - ) + modifier = Modifier + .zIndex(-1f) + .widthIn(max = 320.dp) + ) { + TimelineItemEventContentView( + content = event.content, + isMine = event.isMine, + isEditable = event.isEditable, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + extraPadding = noExtraPadding, + eventSink = eventSink, + modifier = Modifier.defaultTimelineContentPadding() + ) + } } + TimelineItemReadReceiptView( + state = ReadReceiptViewState( + sendState = event.localSendState, + isLastOutgoingMessage = isLastOutgoingMessage, + receipts = event.readReceiptState.receipts, + ), + showReadReceipts = showReadReceipts, + onReadReceiptsClicked = { onReadReceiptsClick(event) }, + ) } } @@ -87,11 +110,17 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview { event = aTimelineItemEvent( isMine = false, content = aTimelineItemStateEventContent(), - groupPosition = TimelineItemGroupPosition.None + groupPosition = TimelineItemGroupPosition.None, + readReceiptState = TimelineItemReadReceipts( + receipts = listOf(aReadReceiptData(0)).toPersistentList(), + ) ), + showReadReceipts = true, + isLastOutgoingMessage = false, isHighlighted = false, onClick = {}, onLongClick = {}, + onReadReceiptsClick = {}, eventSink = {} ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt index 8a4d52c358..1d50509844 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/group/GroupHeaderView.kt @@ -16,6 +16,8 @@ package io.element.android.features.messages.impl.timeline.components.group +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -26,6 +28,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape 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.clip @@ -76,9 +79,17 @@ fun GroupHeaderView( color = MaterialTheme.colorScheme.secondary, style = ElementTheme.typography.fontBodyMdRegular, ) + val rotation: Float by animateFloatAsState( + targetValue = if (isExpanded) 90f else 0f, + animationSpec = tween( + delayMillis = 0, + durationMillis = 300, + ), + label = "chevron" + ) Icon( - modifier = Modifier.rotate(if (isExpanded) 180f else 0f), - imageVector = CompoundIcons.ChevronDown, + modifier = Modifier.rotate(rotation), + imageVector = CompoundIcons.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.secondary ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt index e75e49c1e3..7c30350cc4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenter.kt @@ -17,7 +17,6 @@ package io.element.android.features.messages.impl.timeline.components.reactionsummary 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 @@ -38,10 +37,6 @@ class ReactionSummaryPresenter @Inject constructor( ) : Presenter { @Composable override fun present(): ReactionSummaryState { - LaunchedEffect(Unit) { - room.updateMembers() - } - val membersState by room.membersStateFlow.collectAsState() val target: MutableState = remember { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt index 5c9e4d6ca9..a40e94c529 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt @@ -33,23 +33,23 @@ class ReadReceiptViewStateProvider : PreviewParameterProvider().apply { repeat(1) { add(aReadReceiptData(it)) } }, + receipts = List(1) { aReadReceiptData(it) }, ), aReadReceiptViewState( sendState = LocalEventSendState.Sent(EventId("\$eventId")), - receipts = mutableListOf().apply { repeat(2) { add(aReadReceiptData(it)) } }, + receipts = List(2) { aReadReceiptData(it) }, ), aReadReceiptViewState( sendState = LocalEventSendState.Sent(EventId("\$eventId")), - receipts = mutableListOf().apply { repeat(3) { add(aReadReceiptData(it)) } }, + receipts = List(3) { aReadReceiptData(it) }, ), aReadReceiptViewState( sendState = LocalEventSendState.Sent(EventId("\$eventId")), - receipts = mutableListOf().apply { repeat(4) { add(aReadReceiptData(it)) } }, + receipts = List(4) { aReadReceiptData(it) }, ), aReadReceiptViewState( sendState = LocalEventSendState.Sent(EventId("\$eventId")), - receipts = mutableListOf().apply { repeat(5) { add(aReadReceiptData(it)) } }, + receipts = List(5) { aReadReceiptData(it) }, ), ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt index 823899d8aa..be63fa3409 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt @@ -18,9 +18,10 @@ package io.element.android.features.messages.impl.timeline.components.receipt.bo import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -29,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -38,7 +40,6 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.MatrixUserRow -import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch @@ -83,34 +84,39 @@ internal fun ReadReceiptBottomSheet( } @Composable -private fun ColumnScope.ReadReceiptBottomSheetContent( +private fun ReadReceiptBottomSheetContent( state: ReadReceiptBottomSheetState, onUserDataClicked: (UserId) -> Unit, ) { - ListItem( - headlineContent = { - Text(text = stringResource(id = CommonStrings.common_seen_by)) + LazyColumn { + item { + ListItem( + headlineContent = { + Text(text = stringResource(id = CommonStrings.common_seen_by)) + } + ) + } + items( + items = state.selectedEvent?.readReceiptState?.receipts.orEmpty() + ) { + val userId = UserId(it.avatarData.id) + MatrixUserRow( + modifier = Modifier.clickable { onUserDataClicked(userId) }, + matrixUser = MatrixUser( + userId = userId, + displayName = it.avatarData.name, + avatarUrl = it.avatarData.url, + ), + avatarSize = AvatarSize.ReadReceiptList, + trailingContent = { + Text( + text = it.formattedDate, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } + ) } - ) - val receipts = state.selectedEvent?.readReceiptState?.receipts.orEmpty() - receipts.forEach { - val userId = UserId(it.avatarData.id) - MatrixUserRow( - modifier = Modifier.clickable { onUserDataClicked(userId) }, - matrixUser = MatrixUser( - userId = userId, - displayName = it.avatarData.name, - avatarUrl = it.avatarData.url, - ), - avatarSize = AvatarSize.ReadReceiptList, - trailingContent = { - Text( - text = it.formattedDate, - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, - ) - } - ) } } 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 9fda8b8548..cb11a48d7a 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 @@ -27,7 +27,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt 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.timeline.model.event.TimelineItemVoiceContent -import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -45,14 +44,15 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.ui.messages.toHtmlDocument +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import java.time.Duration import javax.inject.Inject +import kotlin.time.Duration class TimelineItemContentMessageFactory @Inject constructor( private val fileSizeFormatter: FileSizeFormatter, - private val fileExtensionExtractor: FileExtensionExtractor, + private val fileExtensionExtractor: io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor, private val featureFlagService: FeatureFlagService, ) { @@ -104,7 +104,7 @@ class TimelineItemContentMessageFactory @Inject constructor( mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, width = messageType.info?.width?.toInt(), height = messageType.info?.height?.toInt(), - duration = messageType.info?.duration?.toMillis() ?: 0L, + duration = messageType.info?.duration ?: Duration.ZERO, blurHash = messageType.info?.blurhash, aspectRatio = aspectRatio, formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index e9e9af6445..f819be4161 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -73,7 +73,8 @@ private fun MutableList.addGroup( add( TimelineItem.GroupedEvents( id = groupId, - events = groupOfItems.toImmutableList() + events = groupOfItems.toImmutableList(), + aggregatedReadReceipts = groupOfItems.flatMap { it.readReceiptState.receipts }.toImmutableList() ) ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt new file mode 100644 index 0000000000..63775d5ac2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.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.features.messages.impl.timeline.model + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +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.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +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.AttachmentThumbnailInfo +import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import io.element.android.libraries.ui.strings.CommonStrings + +@Immutable +internal sealed interface InReplyToMetadata { + + val text: String? + + data class Thumbnail( + val attachmentThumbnailInfo: AttachmentThumbnailInfo + ) : InReplyToMetadata { + override val text: String? = attachmentThumbnailInfo.textContent + } + + data class Text( + override val text: String + ) : InReplyToMetadata +} + +/** + * Computes metadata for the in reply to message. + * + * Metadata can be either a thumbnail with a text OR just a text. + */ +@Composable +internal fun InReplyToDetails.metadata(): InReplyToMetadata? = when (eventContent) { + is MessageContent -> when (val type = eventContent.type) { + is ImageMessageType -> InReplyToMetadata.Thumbnail( + AttachmentThumbnailInfo( + thumbnailSource = type.info?.thumbnailSource ?: type.source, + textContent = eventContent.body, + type = AttachmentThumbnailType.Image, + blurHash = type.info?.blurhash, + ) + ) + is VideoMessageType -> InReplyToMetadata.Thumbnail( + AttachmentThumbnailInfo( + thumbnailSource = type.info?.thumbnailSource, + textContent = eventContent.body, + type = AttachmentThumbnailType.Video, + blurHash = type.info?.blurhash, + ) + ) + is FileMessageType -> InReplyToMetadata.Thumbnail( + AttachmentThumbnailInfo( + thumbnailSource = type.info?.thumbnailSource, + textContent = eventContent.body, + type = AttachmentThumbnailType.File, + ) + ) + is LocationMessageType -> InReplyToMetadata.Thumbnail( + AttachmentThumbnailInfo( + textContent = stringResource(CommonStrings.common_shared_location), + type = AttachmentThumbnailType.Location, + ) + ) + is AudioMessageType -> InReplyToMetadata.Thumbnail( + AttachmentThumbnailInfo( + textContent = eventContent.body, + type = AttachmentThumbnailType.Audio, + ) + ) + is VoiceMessageType -> InReplyToMetadata.Thumbnail( + AttachmentThumbnailInfo( + textContent = stringResource(CommonStrings.common_voice_message), + type = AttachmentThumbnailType.Voice, + ) + ) + else -> InReplyToMetadata.Text(textContent ?: eventContent.body) + } + is PollContent -> InReplyToMetadata.Thumbnail( + AttachmentThumbnailInfo( + textContent = eventContent.question, + type = AttachmentThumbnailType.Poll, + ) + ) + else -> null +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPositionProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/NewEventState.kt similarity index 63% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPositionProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/NewEventState.kt index 8d8d1231ec..9a00c9fd8f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPositionProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/NewEventState.kt @@ -16,13 +16,10 @@ package io.element.android.features.messages.impl.timeline.model -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -internal class TimelineItemGroupPositionProvider : PreviewParameterProvider { - override val values = sequenceOf( - TimelineItemGroupPosition.First, - TimelineItemGroupPosition.Middle, - TimelineItemGroupPosition.Last, - TimelineItemGroupPosition.None, - ) +/** + * Model if there is a new event in the timeline and if it is from me or from other. + * This can be used to scroll to the bottom of the list when a new event is added. + */ +enum class NewEventState { + None, FromMe, FromOther } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 68dc8a0790..916a9b88b4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -88,6 +88,6 @@ sealed interface TimelineItem { data class GroupedEvents( val id: String, val events: ImmutableList, + val aggregatedReadReceipts: ImmutableList, ) : TimelineItem - } 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 9d9a41e0e3..aa228efcbd 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 @@ -16,9 +16,8 @@ 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 +import kotlin.time.Duration data class TimelineItemAudioContent( val body: String, @@ -29,6 +28,10 @@ data class TimelineItemAudioContent( val fileExtension: String, ) : TimelineItemEventContent { - val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize) + val fileExtensionAndSize = + io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize( + fileExtension, + formattedFileSize + ) override val type: String = "TimelineItemAudioContent" } 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 06cb53b6fe..09c553730d 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,7 +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 +import kotlin.time.Duration.Companion.milliseconds open class TimelineItemAudioContentProvider : PreviewParameterProvider { override val values: Sequence @@ -35,6 +35,6 @@ fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAu mimeType = MimeTypes.Pdf, formattedFileSize = "100kB", fileExtension = "mp3", - duration = Duration.ofMillis(100), + duration = 100.milliseconds, mediaSource = MediaSource(""), ) 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 bbbd8748a2..3a4516489e 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 @@ -79,6 +79,8 @@ fun aTimelineItemTextContent() = TimelineItemTextContent( fun aTimelineItemUnknownContent() = TimelineItemUnknownContent -fun aTimelineItemStateEventContent() = TimelineItemStateEventContent( - body = "A state event", +fun aTimelineItemStateEventContent( + body: String = "A state event", +) = TimelineItemStateEventContent( + body = body, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt index aa35f5a117..04f651a8ad 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt @@ -16,8 +16,8 @@ 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 io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize data class TimelineItemFileContent( val body: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt index 14ec0a972d..5519fbc7f1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt @@ -17,10 +17,11 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.media.MediaSource +import kotlin.time.Duration data class TimelineItemVideoContent( val body: String, - val duration: Long, + val duration: Duration, val videoSource: MediaSource, val thumbnailSource: MediaSource?, val aspectRatio: Float?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt index adff01bdc8..939636fadc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt @@ -20,6 +20,7 @@ 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 io.element.android.libraries.matrix.ui.components.A_BLUR_HASH +import kotlin.time.Duration.Companion.milliseconds open class TimelineItemVideoContentProvider : PreviewParameterProvider { override val values: Sequence @@ -35,7 +36,7 @@ fun aTimelineItemVideoContent() = TimelineItemVideoContent( thumbnailSource = null, blurHash = A_BLUR_HASH, aspectRatio = 0.5f, - duration = 100, + duration = 100.milliseconds, videoSource = MediaSource(""), height = 300, width = 150, 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 0d3bb903de..395b5dd2b3 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 @@ -19,7 +19,7 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import kotlinx.collections.immutable.ImmutableList -import java.time.Duration +import kotlin.time.Duration data class TimelineItemVoiceContent( val eventId: EventId?, 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 830255f06e..f27922a4dd 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 @@ -21,21 +21,23 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import kotlinx.collections.immutable.toPersistentList -import java.time.Duration +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes open class TimelineItemVoiceContentProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemVoiceContent( - durationMs = 1, + duration = 1.milliseconds, waveform = listOf(), ), aTimelineItemVoiceContent( - durationMs = 10_000, + duration = 10_000.milliseconds, waveform = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), ), aTimelineItemVoiceContent( - durationMs = 1_800_000, // 30 minutes + duration = 30.minutes, waveform = List(1024) { it / 1024f }, ), ) @@ -44,14 +46,14 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider = 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, - duration = Duration.ofMillis(durationMs), + duration = duration, mediaSource = MediaSource(contentUri), mimeType = mimeType, waveform = waveform.toPersistentList(), 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 584ec96f2a..0e1b960ecc 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,6 +16,7 @@ package io.element.android.features.messages.impl.voicemessages.composer +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -41,7 +42,7 @@ class VoiceMessageComposerPlayer @Inject constructor( private val coroutineScope: CoroutineScope, ) { companion object { - const val MIME_TYPE = "audio/ogg" + const val MIME_TYPE = MimeTypes.Ogg } private var mediaPath: String? = null @@ -201,6 +202,7 @@ class VoiceMessageComposerPlayer @Inject constructor( progress = 0f, ) } + /** * Whether this player is currently playing. */ @@ -212,7 +214,6 @@ class VoiceMessageComposerPlayer @Inject constructor( val isStopped get() = this.playState == PlayState.Stopped } - enum class PlayState { /** * The player is stopped, i.e. it has just been initialised. 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 a6f002c38a..76180397f4 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,6 +17,7 @@ package io.element.android.features.messages.impl.voicemessages.timeline import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.mimetype.MimeTypes 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 @@ -196,7 +197,7 @@ class DefaultVoiceMessagePlayer( mediaPlayer.setMedia( 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. + mimeType = MimeTypes.Ogg, // Files in the voice cache have no extension so we need to set the mime type manually. startPositionMs = if (state.isEnded) 0L else state.currentPosition, ) } 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 aebed9dd57..fa6632308a 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 @@ -98,7 +98,7 @@ class VoiceMessagePresenter @AssistedInject constructor( } } val duration by remember { - derivedStateOf { playerState.duration ?: content.duration.toMillis() } + derivedStateOf { playerState.duration ?: content.duration.inWholeMilliseconds } } val progress by remember { derivedStateOf { 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 d8231ab1ee..62a5ceb29b 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -45,6 +45,7 @@ "Při načítání nastavení oznámení došlo k chybě." "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áš domovský server tuto možnost nepodporuje v šifrovaných místnostech, v této místnosti nebudete dostávat upozornění." "Všechny zprávy" "V této místnosti mě upozornit na" "Zobrazit méně" 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 2c9ab7574c..061cd73f41 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -44,6 +44,7 @@ "Une erreur s’est produite lors du chargement des paramètres de notification." "Échec de la restauration du mode par défaut, veuillez réessayer." "Échec de la configuration du mode, veuillez réessayer." + "Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous ne serez pas notifié(e) dans ce salon." "Tous les messages" "Dans ce salon, prévenez-moi pour" "Afficher moins" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 5410ac0695..1bed98ae06 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -27,7 +27,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory -import io.element.android.features.messages.impl.media.FakeLocalMediaFactory import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter @@ -80,6 +79,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory @@ -92,12 +92,14 @@ import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import kotlin.time.Duration.Companion.milliseconds +@Suppress("LargeClass") class MessagesPresenterTest { @get:Rule @@ -125,6 +127,21 @@ class MessagesPresenterTest { } } + @Test + fun `present - call is disabled if user cannot join it even if there is an ongoing call`() = runTest { + val room = FakeMatrixRoom().apply { + givenCanUserJoinCall(Result.success(false)) + givenRoomInfo(aRoomInfo(hasRoomCall = true)) + } + val presenter = createMessagesPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = consumeItemsUntilTimeout().last() + assertThat(initialState.callState).isEqualTo(RoomCallState.DISABLED) + } + } + @Test fun `present - handle toggling a reaction`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) @@ -264,7 +281,7 @@ class MessagesPresenterTest { val mediaMessage = aMessageEvent( content = TimelineItemVideoContent( body = "video.mp4", - duration = 10L, + duration = 10.milliseconds, videoSource = MediaSource(AN_AVATAR_URL), thumbnailSource = MediaSource(AN_AVATAR_URL), mimeType = MimeTypes.Mp4, @@ -462,7 +479,7 @@ class MessagesPresenterTest { val room = FakeMatrixRoom(sessionId = A_SESSION_ID) room.givenRoomMembersState( MatrixRoomMembersState.Ready( - listOf( + persistentListOf( aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE), ) @@ -489,7 +506,7 @@ class MessagesPresenterTest { room.givenRoomMembersState( MatrixRoomMembersState.Error( failure = Throwable(), - prevRoomMembers = listOf( + prevRoomMembers = persistentListOf( aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE), ) @@ -535,7 +552,7 @@ class MessagesPresenterTest { val room = FakeMatrixRoom(sessionId = A_SESSION_ID) room.givenRoomMembersState( MatrixRoomMembersState.Ready( - listOf( + persistentListOf( aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN), aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE), ) @@ -655,6 +672,7 @@ class MessagesPresenterTest { clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), + currentSessionIdHolder: CurrentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), ): MessagesPresenter { val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) @@ -723,6 +741,7 @@ class MessagesPresenterTest { featureFlagsService = FakeFeatureFlagService(), buildMeta = aBuildMeta(), dispatchers = coroutineDispatchers, + currentSessionIdHolder = currentSessionIdHolder, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt index 07d6963441..5b8afcfe37 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt @@ -26,13 +26,13 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter import io.element.android.features.messages.impl.attachments.preview.SendActionState -import io.element.android.features.messages.impl.fixtures.aLocalMedia -import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia import io.element.android.tests.testutils.WarmUpRule import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/media.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/aMediaAttachment.kt similarity index 71% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/media.kt rename to features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/aMediaAttachment.kt index ad1afc1af7..fb6eabfee1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/media.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/aMediaAttachment.kt @@ -16,22 +16,10 @@ package io.element.android.features.messages.impl.fixtures -import android.net.Uri import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.MediaInfo -import io.element.android.features.messages.impl.media.local.anImageInfo - -fun aLocalMedia( - uri: Uri, - mediaInfo: MediaInfo = anImageInfo(), -) = LocalMedia( - uri = uri, - info = mediaInfo -) +import io.element.android.libraries.mediaviewer.api.local.LocalMedia fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media( localMedia = localMedia, compressIfPossible = compressIfPossible, ) - diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/timelineItemsFactory.kt index 18ecb4aea9..879333f690 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/timelineItemsFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/timelineItemsFactory.kt @@ -32,7 +32,6 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper -import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter @@ -40,6 +39,7 @@ import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index 02b5b12f2e..ef7891b953 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -26,7 +26,6 @@ import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Composer -import io.element.android.features.messages.impl.media.FakeLocalMediaFactory import io.element.android.features.messages.impl.mentions.MentionSuggestion import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl @@ -67,6 +66,7 @@ import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory @@ -78,11 +78,11 @@ import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest -import okhttp3.internal.immutableListOf import org.junit.Rule import org.junit.Test import uniffi.wysiwyg_composer.MentionsState @@ -734,7 +734,7 @@ class MessageComposerPresenterTest { isOneToOne = false, ).apply { givenRoomMembersState(MatrixRoomMembersState.Ready( - immutableListOf(currentUser, invitedUser, bob, david), + persistentListOf(currentUser, invitedUser, bob, david), )) givenCanTriggerRoomNotification(Result.success(true)) } @@ -798,7 +798,7 @@ class MessageComposerPresenterTest { isOneToOne = true, ).apply { givenRoomMembersState(MatrixRoomMembersState.Ready( - immutableListOf(currentUser, invitedUser, bob, david), + persistentListOf(currentUser, invitedUser, bob, david), )) givenCanTriggerRoomNotification(Result.success(true)) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index ef2b566569..dea1975c6c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.FakeMessagesNavigator import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory +import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager @@ -36,6 +37,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -48,9 +50,12 @@ import io.element.android.libraries.matrix.test.verification.FakeSessionVerifica import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.awaitWithLatch +import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.waitForPredicate +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -187,28 +192,49 @@ class TimelinePresenterTest { } @Test - fun `present - covers hasNewItems scenarios`() = runTest { + fun `present - covers newEventState scenarios`() = runTest { val timeline = FakeMatrixTimeline() val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.hasNewItems).isFalse() + assertThat(initialState.newEventState).isEqualTo(NewEventState.None) assertThat(initialState.timelineItems.size).isEqualTo(0) timeline.updateTimelineItems { listOf(MatrixTimelineItem.Event(0, anEventTimelineItem(content = aMessageContent()))) } - skipItems(1) - assertThat(awaitItem().timelineItems.size).isEqualTo(1) + consumeItemsUntilPredicate { it.timelineItems.size == 1 } + // Mimics sending a message, and assert newEventState is FromMe timeline.updateTimelineItems { items -> - items + listOf(MatrixTimelineItem.Event(1, anEventTimelineItem(content = aMessageContent()))) + val event = anEventTimelineItem(content = aMessageContent(), localSendState = LocalEventSendState.Sent(AN_EVENT_ID)) + items + listOf(MatrixTimelineItem.Event(1, event)) } - skipItems(1) - assertThat(awaitItem().timelineItems.size).isEqualTo(2) - assertThat(awaitItem().hasNewItems).isTrue() + consumeItemsUntilPredicate { it.timelineItems.size == 2 } + awaitLastSequentialItem().also { state -> + assertThat(state.newEventState).isEqualTo(NewEventState.FromMe) + } + // Mimics receiving a message without clearing the previous FromMe + timeline.updateTimelineItems { items -> + val event = anEventTimelineItem(content = aMessageContent()) + items + listOf(MatrixTimelineItem.Event(2, event)) + } + consumeItemsUntilPredicate { it.timelineItems.size == 3 } + + // Scroll to bottom to clear previous FromMe initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) - assertThat(awaitItem().hasNewItems).isFalse() + awaitLastSequentialItem().also { state -> + assertThat(state.newEventState).isEqualTo(NewEventState.None) + } + // Mimics receiving a message and assert newEventState is FromOther + timeline.updateTimelineItems { items -> + val event = anEventTimelineItem(content = aMessageContent()) + items + listOf(MatrixTimelineItem.Event(3, event)) + } + consumeItemsUntilPredicate { it.timelineItems.size == 4 } + awaitLastSequentialItem().also { state -> + assertThat(state.newEventState).isEqualTo(NewEventState.FromOther) + } cancelAndIgnoreRemainingEvents() } } @@ -221,7 +247,7 @@ class TimelinePresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.hasNewItems).isFalse() + assertThat(initialState.newEventState).isEqualTo(NewEventState.None) assertThat(initialState.timelineItems.size).isEqualTo(0) val now = Date().time val minuteInMillis = 60 * 1000 @@ -229,18 +255,18 @@ class TimelinePresenterTest { val (alice, bob, charlie) = aMatrixUserList().take(3).mapIndexed { i, user -> ReactionSender(senderId = user.userId, timestamp = now + i * minuteInMillis) } - val oneReaction = listOf( + val oneReaction = persistentListOf( EventReaction( key = "❤️", - senders = listOf(alice, charlie) + senders = persistentListOf(alice, charlie) ), EventReaction( key = "👍", - senders = listOf(alice, bob) + senders = persistentListOf(alice, bob) ), EventReaction( key = "🐶", - senders = listOf(charlie) + senders = persistentListOf(charlie) ), ) timeline.updateTimelineItems { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt index 080dfaeba6..9d70e03fcd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryPresenterTests.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -42,7 +43,7 @@ class ReactionSummaryPresenterTests { private val roomMember = aRoomMember(userId = A_USER_ID, avatarUrl = AN_AVATAR_URL, displayName = A_USER_NAME) private val summaryEvent = ReactionSummaryEvents.ShowReactionSummary(AN_EVENT_ID, listOf(aggregatedReaction), aggregatedReaction.key) private val room = FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) + givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember))) } private val presenter = ReactionSummaryPresenter(room) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt index e695107e10..a397acafcd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt @@ -27,7 +27,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt 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.timeline.model.event.TimelineItemVoiceContent -import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -55,11 +54,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.test.runTest import org.junit.Test -import java.time.Duration -import java.time.Duration.ofMinutes +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes class TimelineItemContentMessageFactoryTest { @@ -140,14 +141,14 @@ class TimelineItemContentMessageFactoryTest { ) val expected = TimelineItemVideoContent( body = "body", - duration = 0, + duration = Duration.ZERO, videoSource = MediaSource(url = "url", json = null), thumbnailSource = null, aspectRatio = null, blurHash = null, height = null, width = null, - mimeType = "application/octet-stream", + mimeType = MimeTypes.OctetStream, formattedFileSize = "0 Bytes", fileExtension = "", ) @@ -163,7 +164,7 @@ class TimelineItemContentMessageFactoryTest { body = "body.mp4", source = MediaSource("url"), info = VideoInfo( - duration = ofMinutes(1), + duration = 1.minutes, height = 100, width = 300, mimetype = MimeTypes.Mp4, @@ -184,7 +185,7 @@ class TimelineItemContentMessageFactoryTest { ) val expected = TimelineItemVideoContent( body = "body.mp4", - duration = 60_000, + duration = 1.minutes, videoSource = MediaSource(url = "url", json = null), thumbnailSource = MediaSource("url_thumbnail"), aspectRatio = 3f, @@ -210,7 +211,7 @@ class TimelineItemContentMessageFactoryTest { body = "body", duration = Duration.ZERO, mediaSource = MediaSource(url = "url", json = null), - mimeType = "application/octet-stream", + mimeType = MimeTypes.OctetStream, formattedFileSize = "0 Bytes", fileExtension = "", ) @@ -226,7 +227,7 @@ class TimelineItemContentMessageFactoryTest { body = "body.mp3", source = MediaSource("url"), info = AudioInfo( - duration = ofMinutes(1), + duration = 1.minutes, size = 123L, mimetype = MimeTypes.Mp3, ) @@ -237,7 +238,7 @@ class TimelineItemContentMessageFactoryTest { ) val expected = TimelineItemAudioContent( body = "body.mp3", - duration = ofMinutes(1), + duration = 1.minutes, mediaSource = MediaSource(url = "url", json = null), mimeType = MimeTypes.Mp3, formattedFileSize = "123 Bytes", @@ -259,7 +260,7 @@ class TimelineItemContentMessageFactoryTest { body = "body", duration = Duration.ZERO, mediaSource = MediaSource(url = "url", json = null), - mimeType = "application/octet-stream", + mimeType = MimeTypes.OctetStream, waveform = emptyList().toImmutableList() ) assertThat(result).isEqualTo(expected) @@ -274,12 +275,13 @@ class TimelineItemContentMessageFactoryTest { body = "body.ogg", source = MediaSource("url"), info = AudioInfo( - duration = ofMinutes(1), + duration = 1.minutes, size = 123L, mimetype = MimeTypes.Ogg, ), details = AudioDetails( - duration = ofMinutes(1), waveform = listOf(1f, 2f) + duration = 1.minutes, + waveform = persistentListOf(1f, 2f), ), ) ), @@ -289,10 +291,10 @@ class TimelineItemContentMessageFactoryTest { val expected = TimelineItemVoiceContent( eventId = AN_EVENT_ID, body = "body.ogg", - duration = ofMinutes(1), + duration = 1.minutes, mediaSource = MediaSource(url = "url", json = null), mimeType = MimeTypes.Ogg, - waveform = listOf(1f, 2f).toImmutableList() + waveform = persistentListOf(1f, 2f) ) assertThat(result).isEqualTo(expected) } @@ -315,7 +317,7 @@ class TimelineItemContentMessageFactoryTest { body = "body", duration = Duration.ZERO, mediaSource = MediaSource(url = "url", json = null), - mimeType = "application/octet-stream", + mimeType = MimeTypes.OctetStream, formattedFileSize = "0 Bytes", fileExtension = "" ) @@ -336,7 +338,7 @@ class TimelineItemContentMessageFactoryTest { thumbnailSource = null, formattedFileSize = "0 Bytes", fileExtension = "", - mimeType = "application/octet-stream", + mimeType = MimeTypes.OctetStream, blurhash = null, width = null, height = null, @@ -401,7 +403,7 @@ class TimelineItemContentMessageFactoryTest { thumbnailSource = null, formattedFileSize = "0 Bytes", fileExtension = "", - mimeType = "application/octet-stream" + mimeType = MimeTypes.OctetStream ) assertThat(result).isEqualTo(expected) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt index aa5f07800e..1278a598fc 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt @@ -38,6 +38,11 @@ import io.element.android.libraries.matrix.test.A_USER_ID_7 import io.element.android.libraries.matrix.test.A_USER_ID_8 import io.element.android.libraries.matrix.test.A_USER_ID_9 import io.element.android.libraries.matrix.test.FakeMatrixClient +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.test.runTest import org.junit.Test @@ -55,7 +60,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest { - val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() Truth.assertThat( factory.create(aPollContent(votes = votes), eventId = null) ) @@ -87,7 +92,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest { - val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() Truth.assertThat( factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null) ) @@ -106,7 +111,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { - val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } + val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() Truth.assertThat( factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null) ) @@ -136,7 +141,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest { - val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() Truth.assertThat( factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes), eventId = null) ) @@ -172,7 +177,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest { - val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() Truth.assertThat( factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL), eventId = null) ) @@ -192,7 +197,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { - val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } + val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() Truth.assertThat( factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL), eventId = null) ) @@ -221,13 +226,13 @@ internal class TimelineItemContentPollFactoryTest { private fun aPollContent( pollKind: PollKind = PollKind.Disclosed, - votes: Map> = emptyMap(), + votes: ImmutableMap> = persistentMapOf(), endTime: ULong? = null, ): PollContent = PollContent( question = A_POLL_QUESTION, kind = pollKind, maxSelections = 1UL, - answers = listOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4), + answers = persistentListOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4), votes = votes, endTime = endTime, isEdited = false, @@ -277,17 +282,17 @@ internal class TimelineItemContentPollFactoryTest { private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries") private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger") - private val MY_USER_WINNING_VOTES = mapOf( - A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), - A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner - A_POLL_ANSWER_3 to emptyList(), - A_POLL_ANSWER_4 to listOf(A_USER_ID_10), + private val MY_USER_WINNING_VOTES = persistentMapOf( + A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), + A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner + A_POLL_ANSWER_3 to persistentListOf(), + A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_10), ) - private val OTHER_WINNING_VOTES = mapOf( - A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner - A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_6), - A_POLL_ANSWER_3 to emptyList(), - A_POLL_ANSWER_4 to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner + private val OTHER_WINNING_VOTES = persistentMapOf( + A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner + A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_6), + A_POLL_ANSWER_3 to persistentListOf(), + A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index f8579aaf15..1f111f248c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -86,11 +86,12 @@ class TimelineItemGrouperTest { assertThat(result).isEqualTo( listOf( TimelineItem.GroupedEvents( - computeGroupIdWith(aGroupableItem), + id = computeGroupIdWith(aGroupableItem), events = listOf( aGroupableItem.copy("0"), aGroupableItem.copy(id = "1"), - ).toImmutableList() + ).toImmutableList(), + aggregatedReadReceipts = emptyList().toImmutableList(), ), ) ) @@ -132,20 +133,22 @@ class TimelineItemGrouperTest { assertThat(result).isEqualTo( listOf( TimelineItem.GroupedEvents( - computeGroupIdWith(aGroupableItem), + id = computeGroupIdWith(aGroupableItem), events = listOf( aGroupableItem, aGroupableItem, - ).toImmutableList() + ).toImmutableList(), + aggregatedReadReceipts = emptyList().toImmutableList(), ), aNonGroupableItem, TimelineItem.GroupedEvents( - computeGroupIdWith(aGroupableItem), + id = computeGroupIdWith(aGroupableItem), events = listOf( aGroupableItem, aGroupableItem, aGroupableItem, - ).toImmutableList() + ).toImmutableList(), + aggregatedReadReceipts = emptyList().toImmutableList(), ) ) ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadataKtTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadataKtTest.kt new file mode 100644 index 0000000000..4f4fa296bc --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadataKtTest.kt @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.model + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent +import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +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.RedactedContent +import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.media.aMediaSource +import io.element.android.libraries.matrix.test.room.aMessageContent +import io.element.android.libraries.matrix.test.room.aPollContent +import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo +import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class InReplyToMetadataKtTest { + @Test + fun `any message content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetails(eventContent = aMessageContent()).metadata() + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo(InReplyToMetadata.Text("textContent")) + } + } + } + + @Test + fun `an image message content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetails( + eventContent = aMessageContent( + messageType = ImageMessageType( + body = "body", + source = aMediaSource(), + info = null, + ) + ) + ).metadata() + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = aMediaSource(), + textContent = "body", + type = AttachmentThumbnailType.Image, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `a video message content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetails( + eventContent = aMessageContent( + messageType = VideoMessageType( + body = "body", + source = aMediaSource(), + info = VideoInfo( + duration = null, + height = null, + width = null, + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = aMediaSource(), + blurhash = null + ), + ) + ) + ).metadata() + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = aMediaSource(), + textContent = "body", + type = AttachmentThumbnailType.Video, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `a file message content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetails( + eventContent = aMessageContent( + messageType = FileMessageType( + body = "body", + source = aMediaSource(), + info = FileInfo( + mimetype = null, + size = null, + thumbnailInfo = null, + thumbnailSource = aMediaSource(), + ), + ) + ) + ).metadata() + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = aMediaSource(), + textContent = "body", + type = AttachmentThumbnailType.File, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `a audio message content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetails( + eventContent = aMessageContent( + messageType = AudioMessageType( + body = "body", + source = aMediaSource(), + info = AudioInfo( + duration = null, + size = null, + mimetype = null + ), + ) + ) + ).metadata() + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + textContent = "body", + type = AttachmentThumbnailType.Audio, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `a location message content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + testEnv { + anInReplyToDetails( + eventContent = aMessageContent( + messageType = LocationMessageType( + body = "body", + geoUri = "geo:3.0,4.0;u=5.0", + description = null, + ) + ) + ).metadata() + } + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "Shared location", + type = AttachmentThumbnailType.Location, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `a voice message content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + testEnv { + anInReplyToDetails( + eventContent = aMessageContent( + messageType = VoiceMessageType( + body = "body", + source = aMediaSource(), + info = null, + details = null, + ) + ) + ).metadata() + } + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "Voice message", + type = AttachmentThumbnailType.Voice, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `a poll content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetails( + eventContent = aPollContent() + ).metadata() + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo( + InReplyToMetadata.Thumbnail( + attachmentThumbnailInfo = AttachmentThumbnailInfo( + thumbnailSource = null, + textContent = "Do you like polls?", + type = AttachmentThumbnailType.Poll, + blurHash = null, + ) + ) + ) + } + } + } + + @Test + fun `any other content`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + anInReplyToDetails( + eventContent = RedactedContent + ).metadata() + }.test { + awaitItem().let { + Truth.assertThat(it).isEqualTo(null) + } + } + } +} + +fun anInReplyToDetails( + eventId: EventId = AN_EVENT_ID, + senderId: UserId = A_USER_ID, + senderDisplayName: String? = "senderDisplayName", + senderAvatarUrl: String? = "senderAvatarUrl", + eventContent: EventContent? = aMessageContent(), + textContent: String? = "textContent", +) = InReplyToDetails( + eventId = eventId, + senderId = senderId, + senderDisplayName = senderDisplayName, + senderAvatarUrl = senderAvatarUrl, + eventContent = eventContent, + textContent = textContent, +) + +@Composable +private fun testEnv(content: @Composable () -> Any?): Any? { + var result: Any? = null + CompositionLocalProvider( + LocalConfiguration provides Configuration(), + LocalContext provides ApplicationProvider.getApplicationContext(), + ) { + content().apply { + result = this + } + } + return result +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt index d9587b5e9b..e562d66815 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.voicemessages.timeline import com.google.common.truth.Truth +import io.element.android.libraries.core.mimetype.MimeTypes 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 @@ -143,7 +144,7 @@ private fun createDefaultVoiceMessageMediaRepo( url = mxcUri, json = null ), - mimeType = "audio/ogg", + mimeType = MimeTypes.Ogg, body = "someBody.ogg" ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt index 23830fdf39..c84487da1d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.voicemessages.timeline import app.cash.turbine.TurbineTestContext import app.cash.turbine.test import com.google.common.truth.Truth +import io.element.android.libraries.core.mimetype.MimeTypes 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 @@ -287,7 +288,7 @@ private fun createDefaultVoiceMessagePlayer( url = MXC_URI, json = null ), - mimeType = "audio/ogg", + mimeType = MimeTypes.Ogg, body = "someBody.ogg" ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt index 644f1fe231..3a003e23a9 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.voicemessages.timeline import com.google.common.truth.Truth +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo @@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,7 +39,7 @@ class RedactedVoiceMessageManagerTest { @Test fun `redacted event - no playing related media`() = runTest { val mediaPlayer = FakeMediaPlayer().apply { - setMedia(uri = "someUri", mediaId = AN_EVENT_ID.value, mimeType = "audio/ogg") + setMedia(uri = "someUri", mediaId = AN_EVENT_ID.value, mimeType = MimeTypes.Ogg) play() } val manager = aDefaultRedactedVoiceMessageManager(mediaPlayer = mediaPlayer) @@ -54,7 +56,7 @@ class RedactedVoiceMessageManagerTest { @Test fun `redacted event - playing related media is paused`() = runTest { val mediaPlayer = FakeMediaPlayer().apply { - setMedia(uri = "someUri", mediaId = AN_EVENT_ID.value, mimeType = "audio/ogg") + setMedia(uri = "someUri", mediaId = AN_EVENT_ID.value, mimeType = MimeTypes.Ogg) play() } val manager = aDefaultRedactedVoiceMessageManager(mediaPlayer = mediaPlayer) @@ -87,8 +89,8 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf( isOwn = false, isRemote = false, localSendState = null, - reactions = listOf(), - receipts = listOf(), + reactions = persistentListOf(), + receipts = persistentListOf(), sender = A_USER_ID, senderProfile = ProfileTimelineDetails.Unavailable, timestamp = 9442, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenterTest.kt index 299e27ced2..9d885016d8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenterTest.kt @@ -29,6 +29,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds class VoiceMessagePresenterTest { @Test @@ -49,7 +50,7 @@ class VoiceMessagePresenterTest { fun `pressing play downloads and plays`() = runTest { val presenter = createVoiceMessagePresenter( mediaPlayer = FakeMediaPlayer(fakeTotalDurationMs = 2_000), - content = aTimelineItemVoiceContent(durationMs = 2_000), + content = aTimelineItemVoiceContent(duration = 2_000.milliseconds), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -87,7 +88,7 @@ class VoiceMessagePresenterTest { mediaPlayer = FakeMediaPlayer(fakeTotalDurationMs = 2_000), voiceMessageMediaRepo = FakeVoiceMessageMediaRepo().apply { shouldFail = true }, analyticsService = analyticsService, - content = aTimelineItemVoiceContent(durationMs = 2_000), + content = aTimelineItemVoiceContent(duration = 2_000.milliseconds), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -123,7 +124,7 @@ class VoiceMessagePresenterTest { fun `pressing pause while playing pauses`() = runTest { val presenter = createVoiceMessagePresenter( mediaPlayer = FakeMediaPlayer(fakeTotalDurationMs = 2_000), - content = aTimelineItemVoiceContent(durationMs = 2_000), + content = aTimelineItemVoiceContent(duration = 2_000.milliseconds), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -172,7 +173,7 @@ class VoiceMessagePresenterTest { fun `seeking before play`() = runTest { val presenter = createVoiceMessagePresenter( mediaPlayer = FakeMediaPlayer(fakeTotalDurationMs = 2_000), - content = aTimelineItemVoiceContent(durationMs = 10_000), + content = aTimelineItemVoiceContent(duration = 10_000.milliseconds), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -196,7 +197,7 @@ class VoiceMessagePresenterTest { @Test fun `seeking after play`() = runTest { val presenter = createVoiceMessagePresenter( - content = aTimelineItemVoiceContent(durationMs = 10_000), + content = aTimelineItemVoiceContent(duration = 10_000.milliseconds), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollConstants.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollConstants.kt new file mode 100644 index 0000000000..3e75bc2427 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollConstants.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.poll.impl + +internal object PollConstants { + const val MIN_ANSWERS = 2 + const val MAX_ANSWERS = 20 + const val MAX_ANSWER_LENGTH = 240 + const val MAX_SELECTIONS = 1 +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt index b763edffa1..5831a73850 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.poll.PollKind sealed interface CreatePollEvents { data object Save : CreatePollEvents + data class Delete(val confirmed: Boolean) : CreatePollEvents data class SetQuestion(val question: String) : CreatePollEvents data class SetAnswer(val index: Int, val text: String) : CreatePollEvents data object AddAnswer : CreatePollEvents diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt index 58a32584e0..484e92eb15 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt @@ -18,13 +18,11 @@ package io.element.android.features.poll.impl.create import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState 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.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dagger.assisted.Assisted @@ -34,21 +32,19 @@ import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.PollCreation import io.element.android.features.messages.api.MessageComposerContext import io.element.android.features.poll.api.create.CreatePollMode +import io.element.android.features.poll.impl.PollConstants.MAX_SELECTIONS import io.element.android.features.poll.impl.data.PollRepository import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.poll.isDisclosed import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch import timber.log.Timber -private const val MIN_ANSWERS = 2 -private const val MAX_ANSWERS = 20 -private const val MAX_ANSWER_LENGTH = 240 -private const val MAX_SELECTIONS = 1 - class CreatePollPresenter @AssistedInject constructor( private val repository: PollRepository, private val analyticsService: AnalyticsService, @@ -64,17 +60,31 @@ class CreatePollPresenter @AssistedInject constructor( @Composable override fun present(): CreatePollState { - var question: String by rememberSaveable { mutableStateOf("") } - var answers: List by rememberSaveable { mutableStateOf(listOf("", "")) } - var pollKind: PollKind by rememberSaveable(saver = pollKindSaver) { mutableStateOf(PollKind.Disclosed) } - var showConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } + // The initial state of the form. In edit mode this will be populated with the poll being edited. + var initialPoll: PollFormState by rememberSaveable(stateSaver = pollFormStateSaver) { + mutableStateOf(PollFormState.Empty) + } + // The current state of the form. + var poll: PollFormState by rememberSaveable(stateSaver = pollFormStateSaver) { + mutableStateOf(initialPoll) + } + + // Whether the form has been changed from the initial state + val isDirty: Boolean by remember { derivedStateOf { poll != initialPoll } } + + var showBackConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } + var showDeleteConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { if (mode is CreatePollMode.EditPoll) { repository.getPoll(mode.eventId).onSuccess { - question = it.question - answers = it.answers.map(PollAnswer::text) - pollKind = it.kind + val loadedPoll = PollFormState( + question = it.question, + answers = it.answers.map(PollAnswer::text).toPersistentList(), + isDisclosed = it.kind.isDisclosed, + ) + initialPoll = loadedPoll + poll = loadedPoll }.onFailure { analyticsService.trackGetPollFailed(it) navigateUp() @@ -82,9 +92,9 @@ class CreatePollPresenter @AssistedInject constructor( } } - val canSave: Boolean by remember { derivedStateOf { canSave(question, answers) } } - val canAddAnswer: Boolean by remember { derivedStateOf { canAddAnswer(answers) } } - val immutableAnswers: ImmutableList by remember { derivedStateOf { answers.toAnswers() } } + val canSave: Boolean by remember { derivedStateOf { poll.isValid } } + val canAddAnswer: Boolean by remember { derivedStateOf { poll.canAddAnswer } } + val immutableAnswers: ImmutableList by remember { derivedStateOf { poll.toUiAnswers() } } val scope = rememberCoroutineScope() @@ -97,14 +107,14 @@ class CreatePollPresenter @AssistedInject constructor( is CreatePollMode.EditPoll -> mode.eventId is CreatePollMode.NewPoll -> null }, - question = question, - answers = answers, - pollKind = pollKind, + question = poll.question, + answers = poll.answers, + pollKind = poll.pollKind, maxSelections = MAX_SELECTIONS, ).onSuccess { analyticsService.capturePollSaved( - isUndisclosed = pollKind == PollKind.Undisclosed, - numberOfAnswers = answers.size, + isUndisclosed = poll.pollKind == PollKind.Undisclosed, + numberOfAnswers = poll.answers.size, ) }.onFailure { analyticsService.trackSavePollFailed(it, mode) @@ -114,35 +124,52 @@ class CreatePollPresenter @AssistedInject constructor( Timber.d("Cannot create poll") } } - is CreatePollEvents.AddAnswer -> { - answers = answers + "" - } - is CreatePollEvents.RemoveAnswer -> { - answers = answers.filterIndexed { index, _ -> index != event.index } - } - is CreatePollEvents.SetAnswer -> { - answers = answers.toMutableList().apply { - this[event.index] = event.text.take(MAX_ANSWER_LENGTH) + is CreatePollEvents.Delete -> { + if (mode !is CreatePollMode.EditPoll) { + return + } + + if (!event.confirmed) { + showDeleteConfirmation = true + return + } + + scope.launch { + showDeleteConfirmation = false + repository.deletePoll(mode.eventId) + navigateUp() } } + is CreatePollEvents.AddAnswer -> { + poll = poll.withNewAnswer() + } + is CreatePollEvents.RemoveAnswer -> { + poll= poll.withAnswerRemoved(event.index) + } + is CreatePollEvents.SetAnswer -> { + poll = poll.withAnswerChanged(event.index, event.text) + } is CreatePollEvents.SetPollKind -> { - pollKind = event.pollKind + poll = poll.copy(isDisclosed = event.pollKind.isDisclosed) } is CreatePollEvents.SetQuestion -> { - question = event.question + poll = poll.copy(question = event.question) } is CreatePollEvents.NavBack -> { navigateUp() } CreatePollEvents.ConfirmNavBack -> { - val shouldConfirm = question.isNotBlank() || answers.any { it.isNotBlank() } + val shouldConfirm = isDirty if (shouldConfirm) { - showConfirmation = true + showBackConfirmation = true } else { navigateUp() } } - is CreatePollEvents.HideConfirmation -> showConfirmation = false + is CreatePollEvents.HideConfirmation -> { + showBackConfirmation = false + showDeleteConfirmation = false + } } } @@ -153,10 +180,11 @@ class CreatePollPresenter @AssistedInject constructor( }, canSave = canSave, canAddAnswer = canAddAnswer, - question = question, + question = poll.question, answers = immutableAnswers, - pollKind = pollKind, - showConfirmation = showConfirmation, + pollKind = poll.pollKind, + showBackConfirmation = showBackConfirmation, + showDeleteConfirmation = showDeleteConfirmation, eventSink = ::handleEvents, ) } @@ -207,35 +235,12 @@ private fun AnalyticsService.trackSavePollFailed(cause: Throwable, mode: CreateP trackError(exception) } -private fun canSave( - question: String, - answers: List -) = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() } - -private fun canAddAnswer(answers: List) = answers.size < MAX_ANSWERS - -fun List.toAnswers(): ImmutableList { - return map { answer -> +fun PollFormState.toUiAnswers(): ImmutableList { + return answers.map { answer -> Answer( text = answer, - canDelete = this.size > MIN_ANSWERS, + canDelete = canDeleteAnswer, ) }.toImmutableList() } -private val pollKindSaver: Saver, Boolean> = Saver( - save = { - when (it.value) { - PollKind.Disclosed -> false - PollKind.Undisclosed -> true - } - }, - restore = { - mutableStateOf( - when (it) { - true -> PollKind.Undisclosed - else -> PollKind.Disclosed - } - ) - } -) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt index 23069e1d78..d3248be8a5 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt @@ -26,13 +26,16 @@ data class CreatePollState( val question: String, val answers: ImmutableList, val pollKind: PollKind, - val showConfirmation: Boolean, + val showBackConfirmation: Boolean, + val showDeleteConfirmation: Boolean, val eventSink: (CreatePollEvents) -> Unit, ) { enum class Mode { New, Edit, } + + val canDelete: Boolean = mode == Mode.Edit } data class Answer( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt index 0df5293b19..d6d843eff9 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt @@ -34,7 +34,8 @@ class CreatePollStateProvider : PreviewParameterProvider { Answer("", false) ), pollKind = PollKind.Disclosed, - showConfirmation = false, + showBackConfirmation = false, + showDeleteConfirmation = false, ), aCreatePollState( mode = CreatePollState.Mode.New, @@ -45,7 +46,8 @@ class CreatePollStateProvider : PreviewParameterProvider { Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false), Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false), ), - showConfirmation = false, + showBackConfirmation = false, + showDeleteConfirmation = false, pollKind = PollKind.Undisclosed, ), aCreatePollState( @@ -57,7 +59,8 @@ class CreatePollStateProvider : PreviewParameterProvider { Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false), Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false), ), - showConfirmation = true, + showBackConfirmation = true, + showDeleteConfirmation = false, pollKind = PollKind.Undisclosed, ), aCreatePollState( @@ -71,7 +74,8 @@ class CreatePollStateProvider : PreviewParameterProvider { Answer("Brazilian \uD83C\uDDE7\uD83C\uDDF7", true), Answer("French \uD83C\uDDEB\uD83C\uDDF7", true), ), - showConfirmation = false, + showBackConfirmation = false, + showDeleteConfirmation = false, pollKind = PollKind.Undisclosed, ), aCreatePollState( @@ -101,7 +105,8 @@ class CreatePollStateProvider : PreviewParameterProvider { Answer("19", true), Answer("20", true), ), - showConfirmation = false, + showBackConfirmation = false, + showDeleteConfirmation = false, pollKind = PollKind.Undisclosed, ), aCreatePollState( @@ -124,7 +129,8 @@ class CreatePollStateProvider : PreviewParameterProvider { false ), ), - showConfirmation = false, + showBackConfirmation = false, + showDeleteConfirmation = false, pollKind = PollKind.Undisclosed, ), aCreatePollState( @@ -137,7 +143,21 @@ class CreatePollStateProvider : PreviewParameterProvider { Answer("", false) ), pollKind = PollKind.Disclosed, - showConfirmation = false, + showDeleteConfirmation = false, + showBackConfirmation = false, + ), + aCreatePollState( + mode = CreatePollState.Mode.Edit, + canCreate = false, + canAddAnswer = true, + question = "", + answers = persistentListOf( + Answer("", false), + Answer("", false) + ), + pollKind = PollKind.Disclosed, + showDeleteConfirmation = true, + showBackConfirmation = false, ), ) } @@ -148,7 +168,8 @@ private fun aCreatePollState( canAddAnswer: Boolean, question: String, answers: PersistentList, - showConfirmation: Boolean, + showBackConfirmation: Boolean, + showDeleteConfirmation: Boolean, pollKind: PollKind ): CreatePollState { return CreatePollState( @@ -157,7 +178,8 @@ private fun aCreatePollState( canAddAnswer = canAddAnswer, question = question, answers = answers, - showConfirmation = showConfirmation, + showBackConfirmation = showBackConfirmation, + showDeleteConfirmation = showDeleteConfirmation, pollKind = pollKind, eventSink = {} ) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index a0e1d9ec73..fbea82608a 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -76,11 +76,21 @@ fun CreatePollView( val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) } BackHandler(onBack = navBack) - if (state.showConfirmation) ConfirmationDialog( - content = stringResource(id = R.string.screen_create_poll_discard_confirmation), - onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, - onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } - ) + if (state.showBackConfirmation) { + ConfirmationDialog( + content = stringResource(id = R.string.screen_create_poll_cancel_confirmation_content_android), + onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, + onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } + ) + } + if (state.showDeleteConfirmation) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_edit_poll_delete_confirmation_title), + content = stringResource(id = R.string.screen_edit_poll_delete_confirmation), + onSubmitClicked = { state.eventSink(CreatePollEvents.Delete(confirmed = true)) }, + onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } + ) + } val questionFocusRequester = remember { FocusRequester() } val answerFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { @@ -191,6 +201,13 @@ fun CreatePollView( onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) }, ), ) + if (state.canDelete) { + ListItem( + headlineContent = { Text(text = stringResource(id = CommonStrings.action_delete_poll)) }, + style = ListItemStyle.Destructive, + onClick = { state.eventSink(CreatePollEvents.Delete(confirmed = false)) }, + ) + } } } } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt new file mode 100644 index 0000000000..461b96b018 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/PollFormState.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.poll.impl.create + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.mapSaver +import io.element.android.features.poll.impl.PollConstants +import io.element.android.features.poll.impl.PollConstants.MIN_ANSWERS +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +/** + * Represents the state of the poll creation / edit form. + * + * Save this state using [pollFormStateSaver]. + */ +data class PollFormState( + val question: String, + val answers: ImmutableList, + val isDisclosed: Boolean, +) { + companion object { + val Empty = PollFormState( + question = "", + answers = MutableList(MIN_ANSWERS) { "" }.toPersistentList(), + isDisclosed = true, + ) + } + + val pollKind + get() = when (isDisclosed) { + true -> PollKind.Disclosed + false -> PollKind.Undisclosed + } + + /** + * Create a copy of the [PollFormState] with a new blank answer added. + * + * If the maximum number of answers has already been reached an answer is not added. + */ + fun withNewAnswer(): PollFormState { + if (!canAddAnswer) { + return this + } + + return copy(answers = (answers + "").toPersistentList()) + } + + /** + * Create a copy of the [PollFormState] with the answer at [index] removed. + * + * If the answer doesn't exist or can't be removed, the state is unchanged. + * + * @param index the index of the answer to remove. + * + * @return a new [PollFormState] with the answer at [index] removed. + */ + fun withAnswerRemoved(index: Int): PollFormState { + if (!canDeleteAnswer) { + return this + } + + return copy(answers = answers.filterIndexed { i, _ -> i != index }.toPersistentList()) + } + + /** + * Create a copy of the [PollFormState] with the answer at [index] changed. + * + * If the new answer is longer than [PollConstants.MAX_ANSWER_LENGTH], it will be truncated. + * + * @param index the index of the answer to change. + * @param rawAnswer the new answer as the user typed it. + * + * @return a new [PollFormState] with the answer at [index] changed. + */ + fun withAnswerChanged(index: Int, rawAnswer: String): PollFormState = + copy(answers = answers.toMutableList().apply { + this[index] = rawAnswer.take(PollConstants.MAX_ANSWER_LENGTH) + }.toPersistentList()) + + /** + * Whether a new answer can be added. + */ + val canAddAnswer get() = answers.size < PollConstants.MAX_ANSWERS + + /** + * Whether any answer can be deleted. + */ + val canDeleteAnswer get() = answers.size > MIN_ANSWERS + + /** + * Whether the form is currently valid. + */ + val isValid get() = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() } +} + +/** + * A [Saver] for [PollFormState]. + */ +internal val pollFormStateSaver = mapSaver( + save = { + mutableMapOf( + "question" to it.question, + "answers" to it.answers.toTypedArray(), + "isDisclosed" to it.isDisclosed, + ) + }, + restore = { saved -> + PollFormState( + question = saved["question"] as String, + answers = (saved["answers"] as Array<*>).map { it as String }.toPersistentList(), + isDisclosed = saved["isDisclosed"] as Boolean, + ) + } +) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt index d40c5a1df4..0b9aa5aee0 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/data/PollRepository.kt @@ -59,4 +59,11 @@ class PollRepository @Inject constructor( pollKind = pollKind, ) } + + suspend fun deletePoll( + pollStartId: EventId, + ): Result = + room.redactEvent( + eventId = pollStartId, + ) } diff --git a/features/poll/impl/src/main/res/values-cs/translations.xml b/features/poll/impl/src/main/res/values-cs/translations.xml index fa722e9616..a76cf62725 100644 --- a/features/poll/impl/src/main/res/values-cs/translations.xml +++ b/features/poll/impl/src/main/res/values-cs/translations.xml @@ -4,9 +4,11 @@ "Zobrazit výsledky až po skončení hlasování" "Anonymní hlasování" "Volba %1$d" - "Opravdu chcete zrušit toto hlasování?" - "Zrušit hlasování" + "Vaše změny nebyly uloženy. Opravdu se chcete vrátit?" "Otázka nebo téma" "Čeho se hlasování týká?" "Vytvořit hlasování" + "Opravdu chcete odstranit toto hlasování?" + "Odstranit hlasování" + "Upravit hlasování" diff --git a/features/poll/impl/src/main/res/values-de/translations.xml b/features/poll/impl/src/main/res/values-de/translations.xml index bd43eb8337..30b16ec585 100644 --- a/features/poll/impl/src/main/res/values-de/translations.xml +++ b/features/poll/impl/src/main/res/values-de/translations.xml @@ -4,8 +4,6 @@ "Ergebnisse erst nach Ende der Umfrage anzeigen" "Anonyme Umfrage" "Option %1$d" - "Bist du sicher, dass du diese Umfrage verwerfen willst?" - "Umfrage verwerfen" "Frage oder Thema" "Worum geht es bei der Umfrage?" "Umfrage erstellen" diff --git a/features/poll/impl/src/main/res/values-fr/translations.xml b/features/poll/impl/src/main/res/values-fr/translations.xml index adfcfd274d..e323fe5711 100644 --- a/features/poll/impl/src/main/res/values-fr/translations.xml +++ b/features/poll/impl/src/main/res/values-fr/translations.xml @@ -4,8 +4,6 @@ "Afficher les résultats uniquement après la fin du sondage" "Masquer les votes" "Option %1$d" - "Êtes-vous sûr de vouloir supprimer ce sondage ?" - "Supprimer le sondage" "Question ou sujet" "Quel est le sujet du sondage ?" "Créer un sondage" diff --git a/features/poll/impl/src/main/res/values-ro/translations.xml b/features/poll/impl/src/main/res/values-ro/translations.xml index b552a68024..a7ecdcc4dc 100644 --- a/features/poll/impl/src/main/res/values-ro/translations.xml +++ b/features/poll/impl/src/main/res/values-ro/translations.xml @@ -4,8 +4,6 @@ "Afișați rezultatele numai după încheierea sondajului" "Sondaj anonim" "Opțiune %1$d" - "Sunteți sigur că doriți să renunțați la acest sondaj?" - "Renunțați la sondaj" "Întrebare sau subiect" "Despre ce este sondajul?" "Creați un sondaj" diff --git a/features/poll/impl/src/main/res/values-ru/translations.xml b/features/poll/impl/src/main/res/values-ru/translations.xml index c471ff9dd2..22dc506a55 100644 --- a/features/poll/impl/src/main/res/values-ru/translations.xml +++ b/features/poll/impl/src/main/res/values-ru/translations.xml @@ -4,9 +4,11 @@ "Показывать результаты только после окончания опроса" "Анонимный опрос" "Настройка %1$d" - "Вы действительно хотите отменить этот опрос?" - "Отменить опрос" + "Изменения не сохранены. Вы действительно хотите вернуться?" "Вопрос или тема" "Тема опроса?" "Создать опрос" + "Вы уверены, что хотите удалить этот опрос?" + "Удалить опрос" + "Редактировать опрос" diff --git a/features/poll/impl/src/main/res/values-sk/translations.xml b/features/poll/impl/src/main/res/values-sk/translations.xml index 5e2d9ee7bc..c68b82dd32 100644 --- a/features/poll/impl/src/main/res/values-sk/translations.xml +++ b/features/poll/impl/src/main/res/values-sk/translations.xml @@ -4,9 +4,11 @@ "Zobraziť výsledky až po skončení ankety" "Anonymná anketa" "Možnosť %1$d" - "Ste si istí, že chcete túto anketu zahodiť?" - "Odstrániť anketu" + "Vaše zmeny neboli uložené. Určite sa chcete vrátiť späť?" "Otázka alebo téma" "O čom je anketa?" "Vytvoriť anketu" + "Ste si istý, že chcete odstrániť túto anketu?" + "Odstrániť anketu" + "Upraviť anketu" diff --git a/features/poll/impl/src/main/res/values-zh-rTW/translations.xml b/features/poll/impl/src/main/res/values-zh-rTW/translations.xml index 298224c8ad..03bd96aa28 100644 --- a/features/poll/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/poll/impl/src/main/res/values-zh-rTW/translations.xml @@ -4,8 +4,6 @@ "只在投票結束後顯示結果" "隱藏票數" "選項 %1$d" - "您確定要捨棄這項投票嗎?" - "捨棄投票" "問題或主題" "投什麼?" "建立投票" diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml index 3d0c16c4a1..83942df70d 100644 --- a/features/poll/impl/src/main/res/values/localazy.xml +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -4,10 +4,11 @@ "Show results only after poll ends" "Hide votes" "Option %1$d" - "Are you sure you want to discard this poll?" - "Discard Poll" + "Your changes have not been saved. Are you sure you want to go back?" "Question or topic" "What is the poll about?" "Create Poll" + "Are you sure you want to delete this poll?" + "Delete Poll" "Edit poll" diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index 69046ab808..8cb55704d5 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.test.room.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -364,37 +365,117 @@ class CreatePollPresenterTest { } @Test - fun `confirm nav back with blank fields calls nav back lambda`() = runTest { + fun `confirm nav back from new poll with blank fields calls nav back lambda`() = runTest { val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initial = awaitItem() Truth.assertThat(navUpInvocationsCount).isEqualTo(0) - Truth.assertThat(initial.showConfirmation).isFalse() + Truth.assertThat(initial.showBackConfirmation).isFalse() initial.eventSink(CreatePollEvents.ConfirmNavBack) Truth.assertThat(navUpInvocationsCount).isEqualTo(1) } } @Test - fun `confirm nav back with non blank fields shows confirmation dialog and sending hides it`() = runTest { + fun `confirm nav back from new poll with non blank fields shows confirmation dialog and cancelling hides it`() = runTest { val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initial = awaitItem() initial.eventSink(CreatePollEvents.SetQuestion("Non blank")) - Truth.assertThat(navUpInvocationsCount).isEqualTo(0) - Truth.assertThat(awaitItem().showConfirmation).isFalse() + Truth.assertThat(awaitItem().showBackConfirmation).isFalse() initial.eventSink(CreatePollEvents.ConfirmNavBack) - Truth.assertThat(navUpInvocationsCount).isEqualTo(0) - Truth.assertThat(awaitItem().showConfirmation).isTrue() + Truth.assertThat(awaitItem().showBackConfirmation).isTrue() initial.eventSink(CreatePollEvents.HideConfirmation) - Truth.assertThat(awaitItem().showConfirmation).isFalse() + Truth.assertThat(awaitItem().showBackConfirmation).isFalse() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) } } + @Test + fun `confirm nav back from existing poll with unchanged fields calls nav back lambda`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + val loaded = awaitPollLoaded() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + Truth.assertThat(loaded.showBackConfirmation).isFalse() + loaded.eventSink(CreatePollEvents.ConfirmNavBack) + Truth.assertThat(navUpInvocationsCount).isEqualTo(1) + } + } + + @Test + fun `confirm nav back from existing poll with changed fields shows confirmation dialog and cancelling hides it`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + val loaded = awaitPollLoaded() + loaded.eventSink(CreatePollEvents.SetQuestion("CHANGED")) + Truth.assertThat(awaitItem().showBackConfirmation).isFalse() + loaded.eventSink(CreatePollEvents.ConfirmNavBack) + Truth.assertThat(awaitItem().showBackConfirmation).isTrue() + loaded.eventSink(CreatePollEvents.HideConfirmation) + Truth.assertThat(awaitItem().showBackConfirmation).isFalse() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + } + } + + @Test + fun `delete confirms`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false)) + awaitDeleteConfirmation() + Truth.assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull() + } + } + + @Test + fun `delete can be cancelled`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false)) + Truth.assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull() + awaitDeleteConfirmation().eventSink(CreatePollEvents.HideConfirmation) + awaitPollLoaded().apply { + Truth.assertThat(showDeleteConfirmation).isFalse() + } + Truth.assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull() + } + } + + @Test + fun `delete can be confirmed`() = runTest { + val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitDefaultItem() + awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false)) + Truth.assertThat(fakeMatrixRoom.redactEventEventIdParam).isNull() + awaitDeleteConfirmation().eventSink(CreatePollEvents.Delete(confirmed = true)) + awaitPollLoaded().apply { + Truth.assertThat(showDeleteConfirmation).isFalse() + } + Truth.assertThat(fakeMatrixRoom.redactEventEventIdParam).isEqualTo(pollEventId) + } + } + + private suspend fun TurbineTestContext.awaitDefaultItem() = awaitItem().apply { Truth.assertThat(canSave).isFalse() @@ -402,7 +483,13 @@ class CreatePollPresenterTest { Truth.assertThat(question).isEmpty() Truth.assertThat(answers).isEqualTo(listOf(Answer("", false), Answer("", false))) Truth.assertThat(pollKind).isEqualTo(PollKind.Disclosed) - Truth.assertThat(showConfirmation).isFalse() + Truth.assertThat(showBackConfirmation).isFalse() + Truth.assertThat(showDeleteConfirmation).isFalse() + } + + private suspend fun TurbineTestContext.awaitDeleteConfirmation() = + awaitItem().apply { + Truth.assertThat(showDeleteConfirmation).isTrue() } private suspend fun TurbineTestContext.awaitPollLoaded( @@ -453,7 +540,7 @@ class CreatePollPresenterTest { private fun anExistingPoll() = aPollContent( question = "Do you like polls?", - answers = listOf( + answers = persistentListOf( PollAnswer("1", "Yes"), PollAnswer("2", "No"), PollAnswer("2", "Maybe"), diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.kt new file mode 100644 index 0000000000..62b3918372 --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateSaverTest.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.poll.impl.create + +import androidx.compose.runtime.saveable.SaverScope +import com.google.common.truth.Truth.assertThat +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test + +class PollFormStateSaverTest { + companion object { + val CanSaveScope = SaverScope { true } + } + @Test + fun `test save and restore`() { + val state = PollFormState( + question = "question", + answers = listOf("answer1", "answer2").toPersistentList(), + isDisclosed = true, + ) + + val saved = with(CanSaveScope) { + with(pollFormStateSaver) { + save(state) + } + } + + val restored = saved?.let { + pollFormStateSaver.restore(it) + } + + assertThat(restored).isEqualTo(state) + } +} diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt new file mode 100644 index 0000000000..cf97967f23 --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/PollFormStateTest.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.poll.impl.create + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.poll.impl.PollConstants +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.toPersistentList +import org.junit.Test + +class PollFormStateTest { + + @Test + fun `with new answer`() { + val state = PollFormState.Empty + val newState = state.withNewAnswer() + assertThat(newState.answers).isEqualTo(listOf("", "", "")) + } + + @Test + fun `with new answer, given cannot add, doesn't add`() { + val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS) + val newState = state.withNewAnswer() + assertThat(newState).isEqualTo(state) + } + + @Test + fun `with answer deleted, given cannot delete, doesn't delete`() { + val state = PollFormState.Empty + val newState = state.withAnswerRemoved(0) + assertThat(newState).isEqualTo(state) + } + + @Test + fun `with answer deleted, given can delete`() { + val state = PollFormState.Empty.withNewAnswer() + val newState = state.withAnswerRemoved(0) + assertThat(newState).isEqualTo(PollFormState.Empty) + } + + @Test + fun `with answer changed`() { + val state = PollFormState.Empty + val newState = state.withAnswerChanged(1, "New answer") + assertThat(newState).isEqualTo(PollFormState.Empty.copy( + answers = listOf("", "New answer").toPersistentList() + )) + } + + @Test + fun `with answer changed, given it is too long, truncates`() { + val tooLongAnswer = "a".repeat(PollConstants.MAX_ANSWER_LENGTH * 2) + val truncatedAnswer = "a".repeat(PollConstants.MAX_ANSWER_LENGTH) + val state = PollFormState.Empty + val newState = state.withAnswerChanged(1, tooLongAnswer) + assertThat(newState).isEqualTo(PollFormState.Empty.copy( + answers = listOf("", truncatedAnswer).toPersistentList() + )) + } + + @Test + fun `can add answer is true when it does not have max answers`() { + val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS - 1) + assertThat(state.canAddAnswer).isTrue() + } + + @Test + fun `can add answer is false when it has max answers`() { + val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS) + assertThat(state.canAddAnswer).isFalse() + } + + @Test + fun `can delete answer is false when it has min answers`() { + val state = PollFormState.Empty.withBlankAnswers(PollConstants.MIN_ANSWERS) + assertThat(state.canDeleteAnswer).isFalse() + } + + @Test + fun `can delete answer is true when it has more than min answers`() { + val numAnswers = PollConstants.MIN_ANSWERS + 1 + val state = PollFormState.Empty.withBlankAnswers(numAnswers) + assertThat(state.canDeleteAnswer).isTrue() + } + + @Test + fun `is valid is true when it is valid`() { + val state = aValidPollFormState() + assertThat(state.isValid).isTrue() + } + + @Test + fun `is valid is false when question is blank`() { + val state = aValidPollFormState().copy(question = "") + assertThat(state.isValid).isFalse() + } + + @Test + fun `is valid is false when not enough answers`() { + val state = aValidPollFormState().copy(answers = listOf("").toPersistentList()) + assertThat(state.isValid).isFalse() + } + + @Test + fun `is valid is false when one answer is blank`() { + val state = aValidPollFormState().withNewAnswer() + assertThat(state.isValid).isFalse() + } + + @Test + fun `poll kind when is disclosed`() { + val state = PollFormState.Empty.copy(isDisclosed = true) + assertThat(state.pollKind).isEqualTo(PollKind.Disclosed) + } + + @Test + fun `poll kind when is not disclosed`() { + val state = PollFormState.Empty.copy(isDisclosed = false) + assertThat(state.pollKind).isEqualTo(PollKind.Undisclosed) + } +} + + +private fun aValidPollFormState(): PollFormState { + return PollFormState.Empty.copy( + question = "question", + answers = listOf("answer1", "answer2").toPersistentList(), + isDisclosed = true, + ) +} + +private fun PollFormState.withBlankAnswers(numAnswers: Int): PollFormState = + copy(answers = List(numAnswers) { "" }.toPersistentList()) 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 5d8b149676..1be23be3f6 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 @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification 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.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce @@ -84,7 +85,7 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor( return EditDefaultNotificationSettingState( isOneToOne = isOneToOne, mode = mode.value, - roomsWithUserDefinedMode = roomsWithUserDefinedMode.value, + roomsWithUserDefinedMode = roomsWithUserDefinedMode.value.toImmutableList(), changeNotificationSettingAction = changeNotificationSettingAction.value, eventSink = ::handleEvents ) 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 e8590ec27f..60e8dfd10a 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 @@ -19,11 +19,12 @@ 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 +import kotlinx.collections.immutable.ImmutableList data class EditDefaultNotificationSettingState( val isOneToOne: Boolean, val mode: RoomNotificationMode?, - val roomsWithUserDefinedMode: List, + val roomsWithUserDefinedMode: ImmutableList, val changeNotificationSettingAction: Async, val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, ) 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 71fbfb1e1b..e706701f8d 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 @@ -22,6 +22,7 @@ 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 +import kotlinx.collections.immutable.persistentListOf open class EditDefaultNotificationSettingStateProvider: PreviewParameterProvider { override val values: Sequence @@ -39,7 +40,7 @@ private fun anEditDefaultNotificationSettingsState( ) = EditDefaultNotificationSettingState( isOneToOne = isOneToOne, mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, - roomsWithUserDefinedMode = listOf(aRoomSummary()), + roomsWithUserDefinedMode = persistentListOf(aRoomSummary()), changeNotificationSettingAction = changeNotificationSettingAction, eventSink = {} ) diff --git a/features/preferences/impl/src/main/res/values-cs/translations.xml b/features/preferences/impl/src/main/res/values-cs/translations.xml index 49ffdd14a1..a8f8af56ed 100644 --- a/features/preferences/impl/src/main/res/values-cs/translations.xml +++ b/features/preferences/impl/src/main/res/values-cs/translations.xml @@ -6,6 +6,7 @@ "Vývojářský režim" "Povolením získáte přístup k funkcím a funkcím pro vývojáře." "Vypněte editor formátovaného textu pro ruční zadání Markdown." + "Povolit možnost zobrazení zdroje zprávy na časové ose." "Zobrazované jméno" "Vaše zobrazované jméno" "Došlo k neznámé chybě a informace nelze změnit." @@ -30,6 +31,7 @@ Pokud budete pokračovat, některá nastavení se mohou změnit." "Povolit oznámení na tomto zařízení" "Konfigurace nebyla opravena, zkuste to prosím znovu." "Skupinové chaty" + "Váš domovský server tuto možnost v zašifrovaných místnostech nepodporuje, v některých místnostech nemusíte být upozorněni." "Zmínky" "Vše" "Zmínky" 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 a831656698..aca213c957 100644 --- a/features/preferences/impl/src/main/res/values-fr/translations.xml +++ b/features/preferences/impl/src/main/res/values-fr/translations.xml @@ -30,6 +30,7 @@ Si vous continuez, il est possible que certains de vos paramètres soient modifi "Activer les notifications sur cet appareil" "La configuration n’a pas été corrigée, veuillez réessayer." "Discussions de groupe" + "Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons." "Mentions" "Tous" "Mentions" 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 3432902dea..4a008eab47 100644 --- a/features/preferences/impl/src/main/res/values-ru/translations.xml +++ b/features/preferences/impl/src/main/res/values-ru/translations.xml @@ -6,6 +6,7 @@ "Режим разработчика" "Предоставьте разработчикам доступ к функциям и функциональным возможностям." "Отключить редактор форматированного текста и включить Markdown." + "Включить опцию просмотра источника сообщения на временной шкале." "Отображаемое имя" "Ваше отображаемое имя" "Произошла неизвестная ошибка, изменить информацию не удалось." 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 2b8f866255..0464bf8bee 100644 --- a/features/preferences/impl/src/main/res/values-sk/translations.xml +++ b/features/preferences/impl/src/main/res/values-sk/translations.xml @@ -6,6 +6,7 @@ "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." + "Povoliť možnosť zobrazenia zdroja správy na časovej osi." "Zobrazované meno" "Vaše zobrazované meno" "Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť." diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt index e848082e91..a4d06564a8 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLogger.kt @@ -136,7 +136,7 @@ class VectorFileLogger( * * @return The list of files with logs. */ - fun getLogFiles(): List { + private fun getLogFiles(): List { return tryOrNull( onError = { Timber.e(it, "## getLogFiles() failed") } ) { diff --git a/features/rageshake/impl/src/main/res/values-fr/translations.xml b/features/rageshake/impl/src/main/res/values-fr/translations.xml index ed2d9f7e96..6138f6bce5 100644 --- a/features/rageshake/impl/src/main/res/values-fr/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml @@ -5,7 +5,7 @@ "Contactez-moi" "Modifier la capture d’écran" "S’il vous plait, veuillez décrire le problème. Qu’avez-vous fait ? À quoi vous attendiez-vous ? Que s’est-il réellement passé ? Veuillez ajouter le plus de détails possible." - "Décrire le problème" + "Décrire le problème…" "Si possible, veuillez rédiger la description en anglais." "Envoyer des journaux d’incident" "Autoriser à inclure les journaux techniques" diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.kt new file mode 100644 index 0000000000..26e29e1786 --- /dev/null +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/logs/VectorFileLoggerTest.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.features.rageshake.impl.logs + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class VectorFileLoggerTest { + @Test + fun `init VectorFileLogger log debug`() = runTest { + val sut = createVectorFileLogger() + sut.d("A debug log") + } + + @Test + fun `init VectorFileLogger log error`() = runTest { + val sut = createVectorFileLogger() + sut.e(A_THROWABLE, "A debug log") + } + + @Test + fun `reset VectorFileLogger`() = runTest { + val sut = createVectorFileLogger() + sut.reset() + } + + @Test + fun `check getFromTimber`() { + assertThat(VectorFileLogger.getFromTimber()).isNull() + } + + private fun TestScope.createVectorFileLogger() = VectorFileLogger( + context = RuntimeEnvironment.getApplication(), + dispatcher = testCoroutineDispatchers().io, + ) +} 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 0aaac324da..bd1a414294 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 @@ -22,6 +22,7 @@ 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 io.element.android.libraries.matrix.api.core.UserId import kotlinx.parcelize.Parcelize @@ -42,6 +43,7 @@ interface RoomDetailsEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onOpenGlobalNotificationSettings() + fun onOpenRoom(roomId: RoomId) } interface NodeBuilder { diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index a2fdfa18c1..826621b2b5 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) + implementation(projects.libraries.mediaviewer.api) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) @@ -50,6 +51,7 @@ dependencies { api(projects.services.apperror.api) implementation(libs.coil.compose) implementation(projects.features.leaveroom.api) + implementation(projects.features.createroom.api) implementation(projects.services.analytics.api) testImplementation(libs.test.junit) @@ -66,6 +68,7 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(projects.tests.testutils) testImplementation(projects.features.leaveroom.test) + testImplementation(projects.features.createroom.test) ksp(libs.showkase.processor) } 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 dba31ab223..27ddf47f92 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 @@ -34,12 +34,18 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode +import io.element.android.features.roomdetails.impl.members.details.avatar.RoomMemberAvatarPreviewNode import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode 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.core.mimetype.MimeTypes import io.element.android.libraries.di.RoomScope +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.media.MediaSource +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) @@ -79,6 +85,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget + + @Parcelize + data class MemberAvatarPreview(val userName: String, val avatarUrl: String) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -100,6 +109,10 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun openRoomNotificationSettings() { backstack.push(NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = false)) } + + override fun openAvatarPreview(username: String, url: String) { + backstack.push(NavTarget.MemberAvatarPreview(username, url)) + } } createNode(buildContext, listOf(roomDetailsCallback)) } @@ -136,9 +149,35 @@ class RoomDetailsFlowNode @AssistedInject constructor( } is NavTarget.RoomMemberDetails -> { - val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId)) + val callback = object : RoomMemberDetailsNode.Callback { + override fun openAvatarPreview(username: String, avatarUrl: String) { + backstack.push(NavTarget.MemberAvatarPreview(username, avatarUrl)) + } + + override fun onStartDM(roomId: RoomId) { + plugins().forEach { it.onOpenRoom(roomId) } + } + } + val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback) createNode(buildContext, plugins) } + is NavTarget.MemberAvatarPreview -> { + // We need to fake the MimeType here for the viewer to work. + val mimeType = MimeTypes.Images + val input = MediaViewerNode.Inputs( + mediaInfo = MediaInfo( + name = navTarget.userName, + mimeType = mimeType, + formattedFileSize = "", + fileExtension = "" + ), + mediaSource = MediaSource(url = navTarget.avatarUrl), + thumbnailSource = MediaSource(url = navTarget.avatarUrl), + canDownload = false, + canShare = false, + ) + createNode(buildContext, listOf(input)) + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 65c2ed0f89..f3fa9e2645 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -52,6 +52,7 @@ class RoomDetailsNode @AssistedInject constructor( fun openInviteMembers() fun editRoomDetails() fun openRoomNotificationSettings() + fun openAvatarPreview(username: String, url: String) } private val callbacks = plugins() @@ -110,6 +111,10 @@ class RoomDetailsNode @AssistedInject constructor( callbacks.forEach { it.editRoomDetails() } } + private fun openAvatarPreview(username: String, url: String) { + callbacks.forEach { it.openAvatarPreview(username, url) } + } + @Composable override fun View(modifier: Modifier) { val context = LocalContext.current @@ -140,6 +145,7 @@ class RoomDetailsNode @AssistedInject constructor( openRoomMemberList = ::openRoomMemberList, openRoomNotificationSettings = ::openRoomNotificationSettings, invitePeople = ::invitePeople, + openAvatarPreview = ::openAvatarPreview, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index d7e43fdde0..9c7151d4af 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -89,6 +89,7 @@ fun RoomDetailsView( openRoomMemberList: () -> Unit, openRoomNotificationSettings: () -> Unit, invitePeople: () -> Unit, + openAvatarPreview: (username: String, url: String) -> Unit, modifier: Modifier = Modifier, ) { fun onShareMember() { @@ -132,7 +133,12 @@ fun RoomDetailsView( RoomMemberHeaderSection( avatarUrl = state.roomAvatarUrl ?: member.avatarUrl, userId = member.userId.value, - userName = state.roomName + userName = state.roomName, + openAvatarPreview = { + if (member.avatarUrl != null) { + openAvatarPreview(member.displayName ?: member.userId.value, member.avatarUrl!!) + } + }, ) RoomMemberMainActionsSection(onShareUser = ::onShareMember) } @@ -411,5 +417,6 @@ private fun ContentToPreview(state: RoomDetailsState) { openRoomMemberList = {}, openRoomNotificationSettings = {}, invitePeople = {}, + openAvatarPreview = { _, _ -> }, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt index 532e1a8a25..b99319812b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt @@ -85,6 +85,7 @@ private fun PreferenceBlockUser( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block)), onClick = { if (!isLoading) eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) }, trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null, + style = ListItemStyle.Primary, modifier = modifier, ) } else { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt index ca462c6507..c65b63432e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomdetails.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient @@ -33,10 +34,11 @@ object RoomMemberModule { fun provideRoomMemberDetailsPresenterFactory( matrixClient: MatrixClient, room: MatrixRoom, + startDMAction: StartDMAction, ): RoomMemberDetailsPresenter.Factory { return object : RoomMemberDetailsPresenter.Factory { override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(matrixClient, room, roomMemberId) + return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, startDMAction) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index f1e6447a10..4954af8efc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -90,7 +90,7 @@ fun aRoomMember( isIgnored = isIgnored, ) -fun aRoomMemberList() = listOf( +fun aRoomMemberList() = persistentListOf( anAlice(), aBob(), aRoomMember(UserId("@carol:server.org"), "Carol"), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt index 05688c6cf7..75c66f26e2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt @@ -17,6 +17,8 @@ package io.element.android.features.roomdetails.impl.members.details sealed interface RoomMemberDetailsEvents { + data object StartDM : RoomMemberDetailsEvents + data object ClearStartDMState : RoomMemberDetailsEvents data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data object ClearBlockUserError : RoomMemberDetailsEvents diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 54b86f973c..3037136f63 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -17,6 +17,7 @@ package io.element.android.features.roomdetails.impl.members.details import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.lifecycle.subscribe @@ -29,9 +30,11 @@ import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.anvilannotations.ContributesNode import io.element.android.features.roomdetails.impl.R import io.element.android.libraries.androidutils.system.startSharePlainTextIntent +import io.element.android.libraries.architecture.Async 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.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.services.analytics.api.AnalyticsService @@ -46,11 +49,17 @@ class RoomMemberDetailsNode @AssistedInject constructor( presenterFactory: RoomMemberDetailsPresenter.Factory, ) : Node(buildContext, plugins = plugins) { + interface Callback : NodeInputs { + fun openAvatarPreview(username: String, avatarUrl: String) + fun onStartDM(roomId: RoomId) + } + data class RoomMemberDetailsInput( val roomMemberId: UserId ) : NodeInputs private val inputs = inputs() + private val callback = inputs() private val presenter = presenterFactory.create(inputs.roomMemberId) init { @@ -79,12 +88,24 @@ class RoomMemberDetailsNode @AssistedInject constructor( } } + fun onStartDM(roomId: RoomId) { + callback.onStartDM(roomId) + } + val state = presenter.present() + + LaunchedEffect(state.startDmActionState) { + if (state.startDmActionState is Async.Success) { + onStartDM(state.startDmActionState.data) + } + } RoomMemberDetailsView( state = state, modifier = modifier, goBack = this::navigateUp, - onShareUser = ::onShareUser + onShareUser = ::onShareUser, + onDMStarted = ::onStartDM, + openAvatarPreview = callback::openAvatarPreview, ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 3be83a2fef..e82b5f0cb5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -27,11 +27,13 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState @@ -39,9 +41,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class RoomMemberDetailsPresenter @AssistedInject constructor( + @Assisted private val roomMemberId: UserId, private val client: MatrixClient, private val room: MatrixRoom, - @Assisted private val roomMemberId: UserId, + private val startDMAction: StartDMAction, ) : Presenter { interface Factory { @@ -53,6 +56,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() var confirmationDialog by remember { mutableStateOf(null) } val roomMember by room.getRoomMemberAsState(roomMemberId) + val startDmActionState: MutableState> = remember { mutableStateOf(Async.Uninitialized) } // the room member is not really live... val isBlocked: MutableState> = remember(roomMember) { val isIgnored = roomMember?.isIgnored @@ -88,6 +92,14 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( RoomMemberDetailsEvents.ClearBlockUserError -> { isBlocked.value = Async.Success(isBlocked.value.dataOrNull().orFalse()) } + RoomMemberDetailsEvents.StartDM -> { + coroutineScope.launch { + startDMAction.execute(roomMemberId, startDmActionState) + } + } + RoomMemberDetailsEvents.ClearStartDMState -> { + startDmActionState.value = Async.Uninitialized + } } } @@ -108,6 +120,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( userName = userName, avatarUrl = userAvatar, isBlocked = isBlocked.value, + startDmActionState = startDmActionState.value, displayConfirmationDialog = confirmationDialog, isCurrentUser = client.isMe(roomMember?.userId), eventSink = ::handleEvents diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt index 957db2233e..ee9cfe388f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt @@ -17,12 +17,14 @@ package io.element.android.features.roomdetails.impl.members.details import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.RoomId data class RoomMemberDetailsState( val userId: String, val userName: String?, val avatarUrl: String?, val isBlocked: Async, + val startDmActionState: Async, val displayConfirmationDialog: ConfirmationDialog?, val isCurrentUser: Boolean, val eventSink: (RoomMemberDetailsEvents) -> Unit diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt index b14b0e3634..c5710986a6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt @@ -28,6 +28,7 @@ open class RoomMemberDetailsStateProvider : PreviewParameterProvider Unit, + onDMStarted: (RoomId) -> Unit, goBack: () -> Unit, + openAvatarPreview: (username: String, url: String) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( @@ -62,37 +74,45 @@ fun RoomMemberDetailsView( avatarUrl = state.avatarUrl, userId = state.userId, userName = state.userName, + openAvatarPreview = { avatarUrl -> + openAvatarPreview(state.userName ?: state.userId, avatarUrl) + }, ) RoomMemberMainActionsSection(onShareUser = onShareUser) Spacer(modifier = Modifier.height(26.dp)) - // TODO implement send DM - // SendMessageSection(onSendMessage = { - // ... - // }) - if (!state.isCurrentUser) { + StartDMSection(onStartDMClicked = { state.eventSink(RoomMemberDetailsEvents.StartDM) }) BlockUserSection(state) BlockUserDialogs(state) } + AsyncView( + async = state.startDmActionState, + progressText = stringResource(CommonStrings.common_starting_chat), + onSuccess = onDMStarted, + errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) }, + onRetry = { state.eventSink(RoomMemberDetailsEvents.StartDM) }, + onErrorDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearStartDMState) }, + ) } } } -/* @Composable -private fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = Modifier) { - PreferenceCategory(modifier = modifier) { - PreferenceText( - title = stringResource(CommonStrings.action_send_message), - icon = Icons.Outlined.ChatBubbleOutline, - onClick = onSendMessage, - ) - } +private fun StartDMSection( + onStartDMClicked: () -> Unit, + modifier: Modifier = Modifier +) { + ListItem( + headlineContent = { Text(stringResource(CommonStrings.common_direct_chat)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Chat)), + style = ListItemStyle.Primary, + onClick = onStartDMClicked, + modifier = modifier, + ) } - */ @PreviewWithLargeHeight @Composable @@ -110,5 +130,7 @@ private fun ContentToPreview(state: RoomMemberDetailsState) { state = state, onShareUser = {}, goBack = {}, + onDMStarted = {}, + openAvatarPreview = { _, _ -> } ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt index 4bd3f1d0b9..5025f37b65 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt @@ -16,6 +16,7 @@ package io.element.android.features.roomdetails.impl.members.details +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -41,13 +42,16 @@ fun RoomMemberHeaderSection( avatarUrl: String?, userId: String, userName: String?, + openAvatarPreview: (url: String) -> Unit, modifier: Modifier = Modifier ) { Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { Box(modifier = Modifier.size(70.dp)) { Avatar( avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.UserHeader), - modifier = Modifier.fillMaxSize() + modifier = Modifier + .clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) } + .fillMaxSize() ) } Spacer(modifier = Modifier.height(24.dp)) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/avatar/RoomMemberAvatarPreviewNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/avatar/RoomMemberAvatarPreviewNode.kt new file mode 100644 index 0000000000..8d186ae96c --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/avatar/RoomMemberAvatarPreviewNode.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.roomdetails.impl.members.details.avatar + +import com.bumble.appyx.core.modality.BuildContext +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.RoomScope +import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode +import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter + +@ContributesNode(RoomScope::class) +class RoomMemberAvatarPreviewNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: MediaViewerPresenter.Factory, +) : MediaViewerNode(buildContext, plugins, presenterFactory) 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 110257d6ae..0bddcdf7cd 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -36,6 +36,7 @@ "Při načítání nastavení oznámení došlo k chybě." "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áš domovský server tuto možnost nepodporuje v šifrovaných místnostech, v této místnosti nebudete dostávat upozornění." "Všechny zprávy" "V této místnosti mě upozornit na" "Zablokovat" 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 d2b7f22292..dd35f40252 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -35,6 +35,7 @@ "Une erreur s’est produite lors du chargement des paramètres de notification." "Échec de la restauration du mode par défaut, veuillez réessayer." "Échec de la configuration du mode, veuillez réessayer." + "Votre serveur d’accueil ne supporte pas cette option pour les salons chiffrés, vous ne serez pas notifié(e) dans ce salon." "Tous les messages" "Dans ce salon, prévenez-moi pour" "Bloquer" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 5363c6a78e..5382a9b3d1 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -38,6 +38,7 @@ "Your homeserver does not support this option in encrypted rooms, you won\'t get notified in this room." "All messages" "In this room, notify me for" + "An error occurred when trying to start a chat" "Block" "Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime." "Block user" diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 5ab79d2cee..6ffba16377 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.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.features.createroom.test.FakeStartDMAction import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter @@ -45,10 +46,12 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.testCoroutineDispatchers -import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -60,16 +63,16 @@ class RoomDetailsPresenterTests { @get:Rule val warmUpRule = WarmUpRule() - private fun createRoomDetailsPresenter( + private fun TestScope.createRoomDetailsPresenter( room: MatrixRoom, leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), - dispatchers: CoroutineDispatchers, + dispatchers: CoroutineDispatchers = testCoroutineDispatchers(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService() ): RoomDetailsPresenter { val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService) val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(matrixClient, room, roomMemberId) + return RoomMemberDetailsPresenter(roomMemberId, matrixClient, room, FakeStartDMAction()) } } val featureFlagService = FakeFeatureFlagService( @@ -89,7 +92,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state is created from room info`() = runTest { val room = aMatrixRoom() - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -108,7 +111,7 @@ class RoomDetailsPresenterTests { @Test fun `present - initial state with no room name`() = runTest { val room = aMatrixRoom(name = null) - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -127,10 +130,10 @@ class RoomDetailsPresenterTests { isEncrypted = true, isDirect = true, ).apply { - val roomMembers = listOf(myRoomMember, otherRoomMember) + val roomMembers = persistentListOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -164,7 +167,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.success(false)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -179,7 +182,7 @@ class RoomDetailsPresenterTests { val room = aMatrixRoom().apply { givenCanInviteResult(Result.failure(Throwable("Whoops"))) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -197,7 +200,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Whelp"))) givenCanInviteResult(Result.success(false)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -218,7 +221,7 @@ class RoomDetailsPresenterTests { isEncrypted = true, isDirect = true, ).apply { - val roomMembers = listOf(myRoomMember, otherRoomMember) + val roomMembers = persistentListOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) @@ -226,7 +229,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanInviteResult(Result.success(false)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -241,6 +244,7 @@ class RoomDetailsPresenterTests { cancelAndIgnoreRemainingEvents() } } + @Test fun `present - initial state when in a DM with no topic`() = runTest { val myRoomMember = aRoomMember(A_SESSION_ID) @@ -250,12 +254,12 @@ class RoomDetailsPresenterTests { isDirect = true, topic = null, ).apply { - val roomMembers = listOf(myRoomMember, otherRoomMember) + val roomMembers = persistentListOf(myRoomMember, otherRoomMember) givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers)) givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -276,7 +280,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true)) givenCanInviteResult(Result.success(false)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -297,7 +301,7 @@ class RoomDetailsPresenterTests { givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false)) givenCanInviteResult(Result.success(false)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -315,7 +319,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -333,7 +337,7 @@ class RoomDetailsPresenterTests { givenCanInviteResult(Result.success(false)) } - val presenter = createRoomDetailsPresenter(room, dispatchers = testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter(room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -351,7 +355,11 @@ class RoomDetailsPresenterTests { fun `present - leave room event is passed on to leave room presenter`() = runTest { val leaveRoomPresenter = FakeLeaveRoomPresenter() val room = aMatrixRoom() - val presenter = createRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers()) + val presenter = createRoomDetailsPresenter( + room = room, + leaveRoomPresenter = leaveRoomPresenter, + dispatchers = testCoroutineDispatchers() + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -368,14 +376,18 @@ class RoomDetailsPresenterTests { val leaveRoomPresenter = FakeLeaveRoomPresenter() val notificationSettingsService = FakeNotificationSettingsService() val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) - val presenter = createRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + val presenter = createRoomDetailsPresenter( + room = room, + leaveRoomPresenter = leaveRoomPresenter, + notificationSettingsService = notificationSettingsService, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { notificationSettingsService.setRoomNotificationMode(room.roomId, RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) val updatedState = consumeItemsUntilPredicate { - it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY + it.roomNotificationSettings?.mode == RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY }.last() assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) cancelAndIgnoreRemainingEvents() @@ -384,16 +396,15 @@ class RoomDetailsPresenterTests { @Test fun `present - mute room notifications`() = runTest { - val leaveRoomPresenter = FakeLeaveRoomPresenter() val notificationSettingsService = FakeNotificationSettingsService(initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY) val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) - val presenter = createRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(RoomDetailsEvent.MuteNotification) val updatedState = consumeItemsUntilPredicate(timeout = 250.milliseconds) { - it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE + it.roomNotificationSettings?.mode == RoomNotificationMode.MUTE }.last() assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.MUTE) cancelAndIgnoreRemainingEvents() @@ -402,19 +413,18 @@ class RoomDetailsPresenterTests { @Test fun `present - unmute room notifications`() = runTest { - val leaveRoomPresenter = FakeLeaveRoomPresenter() val notificationSettingsService = FakeNotificationSettingsService( initialRoomMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, initialEncryptedGroupDefaultMode = RoomNotificationMode.ALL_MESSAGES ) val room = aMatrixRoom(notificationSettingsService = notificationSettingsService) - val presenter = createRoomDetailsPresenter(room, leaveRoomPresenter, testCoroutineDispatchers(), notificationSettingsService) + val presenter = createRoomDetailsPresenter(room = room, notificationSettingsService = notificationSettingsService) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().eventSink(RoomDetailsEvent.UnmuteNotification) val updatedState = consumeItemsUntilPredicate { - it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES + it.roomNotificationSettings?.mode == RoomNotificationMode.ALL_MESSAGES }.last() assertThat(updatedState.roomNotificationSettings?.mode).isEqualTo(RoomNotificationMode.ALL_MESSAGES) cancelAndIgnoreRemainingEvents() diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt index 4033038b69..9763ed280b 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -167,7 +168,7 @@ internal class RoomInviteMembersPresenterTest { matrixRoom = FakeMatrixRoom().apply { givenRoomMembersState( MatrixRoomMembersState.Ready( - listOf( + persistentListOf( aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), ) @@ -227,7 +228,7 @@ internal class RoomInviteMembersPresenterTest { roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply { givenRoomMembersState( MatrixRoomMembersState.Ready( - listOf( + persistentListOf( aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt index a249e38823..79a8faf3f1 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -20,16 +20,24 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth +import io.element.android.features.createroom.api.StartDMAction +import io.element.android.features.createroom.test.FakeStartDMAction import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.architecture.Async +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.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMember +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.FakeMatrixClient import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -47,9 +55,12 @@ class RoomMemberDetailsPresenterTests { val room = aMatrixRoom().apply { givenUserDisplayNameResult(Result.success("A custom name")) givenUserAvatarUrlResult(Result.success("A custom avatar")) - givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) + givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember))) } - val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) + val presenter = createRoomMemberDetailsPresenter( + room = room, + roomMember = roomMember + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -71,9 +82,12 @@ class RoomMemberDetailsPresenterTests { val room = aMatrixRoom().apply { givenUserDisplayNameResult(Result.failure(Throwable())) givenUserAvatarUrlResult(Result.failure(Throwable())) - givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) + givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember))) } - val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) + val presenter = createRoomMemberDetailsPresenter( + room = room, + roomMember = roomMember + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -91,9 +105,12 @@ class RoomMemberDetailsPresenterTests { val room = aMatrixRoom().apply { givenUserDisplayNameResult(Result.success(null)) givenUserAvatarUrlResult(Result.success(null)) - givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) + givenRoomMembersState(MatrixRoomMembersState.Ready(persistentListOf(roomMember))) } - val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) + val presenter = createRoomMemberDetailsPresenter( + room = room, + roomMember = roomMember + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -107,9 +124,7 @@ class RoomMemberDetailsPresenterTests { @Test fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { - val room = aMatrixRoom() - val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) + val presenter = createRoomMemberDetailsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -128,9 +143,7 @@ class RoomMemberDetailsPresenterTests { @Test fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { - val room = aMatrixRoom() - val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) + val presenter = createRoomMemberDetailsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -147,11 +160,9 @@ class RoomMemberDetailsPresenterTests { @Test fun `present - BlockUser with error`() = runTest { - val room = aMatrixRoom() - val roomMember = aRoomMember() val matrixClient = FakeMatrixClient() matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE)) - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) + val presenter = createRoomMemberDetailsPresenter(client = matrixClient) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -168,9 +179,7 @@ class RoomMemberDetailsPresenterTests { @Test fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { - val room = aMatrixRoom() - val roomMember = aRoomMember() - val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) + val presenter = createRoomMemberDetailsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -186,4 +195,52 @@ class RoomMemberDetailsPresenterTests { ensureAllEventsConsumed() } } + + @Test + fun `present - start DM action complete scenario`() = runTest { + val startDMAction = FakeStartDMAction() + val presenter = createRoomMemberDetailsPresenter(startDMAction = startDMAction) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.startDmActionState).isInstanceOf(Async.Uninitialized::class.java) + val startDMSuccessResult = Async.Success(A_ROOM_ID) + val startDMFailureResult = Async.Failure(A_THROWABLE) + + // Failure + startDMAction.givenExecuteResult(startDMFailureResult) + initialState.eventSink(RoomMemberDetailsEvents.StartDM) + Truth.assertThat(awaitItem().startDmActionState).isInstanceOf(Async.Loading::class.java) + awaitItem().also { state -> + Truth.assertThat(state.startDmActionState).isEqualTo(startDMFailureResult) + state.eventSink(RoomMemberDetailsEvents.ClearStartDMState) + } + + // Success + startDMAction.givenExecuteResult(startDMSuccessResult) + awaitItem().also { state -> + Truth.assertThat(state.startDmActionState).isEqualTo(Async.Uninitialized) + state.eventSink(RoomMemberDetailsEvents.StartDM) + } + Truth.assertThat(awaitItem().startDmActionState).isInstanceOf(Async.Loading::class.java) + awaitItem().also { state -> + Truth.assertThat(state.startDmActionState).isEqualTo(startDMSuccessResult) + } + } + } + + private fun createRoomMemberDetailsPresenter( + client: MatrixClient = FakeMatrixClient(), + room: MatrixRoom = aMatrixRoom(), + roomMember: RoomMember = aRoomMember(), + startDMAction: StartDMAction = FakeStartDMAction() + ): RoomMemberDetailsPresenter { + return RoomMemberDetailsPresenter( + roomMemberId = roomMember.userId, + client = client, + room = room, + startDMAction = startDMAction + ) + } } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt index 3a445e6cdc..cfaf9b8321 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomNotificationMode @Immutable -data class RoomListRoomSummary constructor( +data class RoomListRoomSummary( val id: String, val roomId: RoomId, val name: String = "", 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 index 1bba716bb1..eab2e0450d 100644 --- 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 @@ -129,11 +129,11 @@ class SecureBackupSetupPresenter @AssistedInject constructor( encryptionService.enableRecoveryProgressStateFlow.collect { enableRecoveryProgress -> Timber.tag(loggerTagSetup.value).d("New enableRecoveryProgress: ${enableRecoveryProgress.javaClass.simpleName}") when (enableRecoveryProgress) { - EnableRecoveryProgress.Unknown, + is EnableRecoveryProgress.Starting, + is EnableRecoveryProgress.CreatingBackup, + is EnableRecoveryProgress.CreatingRecoveryKey, is EnableRecoveryProgress.BackingUp, - EnableRecoveryProgress.CreatingBackup, - EnableRecoveryProgress.CreatingRecoveryKey -> - Unit + is EnableRecoveryProgress.RoomKeyUploadError -> Unit is EnableRecoveryProgress.Done -> stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.SdkHasCreatedKey(enableRecoveryProgress.recoveryKey)) } diff --git a/features/securebackup/impl/src/main/res/values-fr/translations.xml b/features/securebackup/impl/src/main/res/values-fr/translations.xml index 3922f2fa78..9f3f304b25 100644 --- a/features/securebackup/impl/src/main/res/values-fr/translations.xml +++ b/features/securebackup/impl/src/main/res/values-fr/translations.xml @@ -22,6 +22,8 @@ "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." + "Veuillez réessayer pour accéder à votre historique chiffré." + "Clé de récupération incorrecte" "Saisissez la clé à 48 caractères." "Saisissez la clé ici…" "Clé de récupération confirmée" 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 c9d3446f2a..1ad0ffca42 100644 --- a/features/verifysession/impl/src/main/res/values-fr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml @@ -9,6 +9,7 @@ "Réessayer la vérification" "Je suis prêt.e" "En attente de correspondance" + "Comparer un groupe unique d’Emojis." "Comparez les emoji uniques en veillant à ce qu’ils apparaissent dans le même ordre." "Ils ne correspondent pas" "Ils correspondent" diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index 403324816f..8f8795ed78 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -132,7 +133,7 @@ class VerifySelfSessionPresenterTests { presenter.present() }.test { requestVerificationAndAwaitVerifyingState(service) - service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(emptyList())) + service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(persistentListOf())) ensureAllEventsConsumed() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c0086a3e9..dd3558a7bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ serialization_json = "1.6.1" showkase = "1.0.2" appyx = "1.4.0" sqldelight = "2.0.0" -wysiwyg = "2.18.0" +wysiwyg = "2.20.0" # DI dagger = "2.48.1" @@ -86,10 +86,10 @@ androidx_activity_activity = { module = "androidx.activity:activity", version.re androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx_startup = "androidx.startup:startup-runtime:1.1.1" androidx_preference = "androidx.preference:preference:1.2.1" -androidx_webkit = "androidx.webkit:webkit:1.8.0" +androidx_webkit = "androidx.webkit:webkit:1.9.0" androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } -androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha11" +androidx_compose_material3 = "androidx.compose.material3:material3:1.2.0-alpha12" androidx_compose_ui = { module = "androidx.compose.ui:ui" } androidx_compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" } androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" } @@ -136,7 +136,8 @@ test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = " coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } -compound = { module = "io.element.android:compound-android", version = "0.0.1" } +coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" } +compound = { module = "io.element.android:compound-android", version = "0.0.2" } datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" } kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6" @@ -146,7 +147,7 @@ jsoup = "org.jsoup:jsoup:1.17.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:1.3.1" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.70" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.73" 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" } @@ -163,11 +164,11 @@ maplibre = "org.maplibre.gl:android-sdk:10.2.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.2" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.2" opusencoder = "io.element.android:opusencoder:1.1.0" -kotlinpoet = "com.squareup:kotlinpoet:1.15.1" +kotlinpoet = "com.squareup:kotlinpoet:1.15.2" # Analytics posthog = "com.posthog.android:posthog:2.0.3" -sentry = "io.sentry:sentry-android:6.34.0" +sentry = "io.sentry:sentry-android:7.0.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:aa14cbcdf81af2746d20a71779ec751f971e1d7f" # Emojibase @@ -205,8 +206,8 @@ kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } anvil = { id = "com.squareup.anvil", version.ref = "anvil" } -detekt = "io.gitlab.arturbosch.detekt:1.23.3" -ktlint = "org.jlleitschuh.gradle.ktlint:11.6.1" +detekt = "io.gitlab.arturbosch.detekt:1.23.4" +ktlint = "org.jlleitschuh.gradle.ktlint:12.0.2" dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12" dependencycheck = "org.owasp.dependencycheck:9.0.1" dependencyanalysis = "com.autonomousapps.dependency-analysis:1.26.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 01f330a93e..a7a990ab2a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=f2b9ed0faf8472cbe469255ae6c86eddb77076c75191741b4a462f33128dd419 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionSha256Sum=c16d517b50dd28b3f5838f0e844b7520b8f1eb610f2f29de7e4e04a1b7c9c79b +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts index 87382a2c3e..94e6d92160 100644 --- a/libraries/androidutils/build.gradle.kts +++ b/libraries/androidutils/build.gradle.kts @@ -1,4 +1,3 @@ - /* * Copyright (c) 2023 New Vector Ltd * @@ -33,6 +32,7 @@ dependencies { implementation(projects.libraries.di) implementation(projects.libraries.core) + implementation(projects.services.toolbox.api) implementation(libs.dagger) implementation(libs.timber) implementation(libs.androidx.corektx) @@ -41,4 +41,12 @@ dependencies { implementation(libs.androidx.exifinterface) implementation(libs.androidx.security.crypto) api(libs.androidx.browser) + + testImplementation(projects.tests.testutils) + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) + testImplementation(libs.coroutines.core) + testImplementation(libs.coroutines.test) + testImplementation(projects.services.toolbox.test) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt index 9cd70febcc..abf55b581c 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatter.kt @@ -22,16 +22,18 @@ import android.text.format.Formatter import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import javax.inject.Inject @ContributesBinding(AppScope::class) class AndroidFileSizeFormatter @Inject constructor( @ApplicationContext private val context: Context, - ) : FileSizeFormatter { + private val sdkIntProvider: BuildVersionSdkIntProvider, +) : FileSizeFormatter { override fun format(fileSize: Long, useShortFormat: Boolean): String { // Since Android O, the system considers that 1kB = 1000 bytes instead of 1024 bytes. // We want to avoid that. - val normalizedSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + val normalizedSize = if (sdkIntProvider.get() <= Build.VERSION_CODES.N) { fileSize } else { // First convert the size diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 08155b2a34..422307ffd9 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -16,8 +16,6 @@ package io.element.android.libraries.androidutils.system -import android.annotation.TargetApi -import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent @@ -31,6 +29,7 @@ import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.RequiresApi import io.element.android.libraries.androidutils.R import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat +import io.element.android.libraries.core.mimetype.MimeTypes @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O @@ -104,19 +103,6 @@ fun Context.openAppSettingsPage( } } -/** - * Shows notification system settings for the given channel id. - */ -@TargetApi(Build.VERSION_CODES.O) -fun Activity.startNotificationChannelSettingsIntent(channelID: String) { - if (!supportNotificationChannels()) return - val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, packageName) - putExtra(Settings.EXTRA_CHANNEL_ID, channelID) - } - startActivity(intent) -} - @RequiresApi(Build.VERSION_CODES.O) fun Context.startInstallFromSourceIntent( activityResultLauncher: ActivityResultLauncher, @@ -140,7 +126,7 @@ fun Context.startSharePlainTextIntent( noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found), ) { val share = Intent(Intent.ACTION_SEND) - share.type = "text/plain" + share.type = MimeTypes.PlainText share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) // Add data to the intent, the receiving app will decide what to do with it. share.putExtra(Intent.EXTRA_SUBJECT, subject) diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatterTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatterTest.kt new file mode 100644 index 0000000000..4d83a2367a --- /dev/null +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/filesize/AndroidFileSizeFormatterTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.filesize + +import android.os.Build +import com.google.common.truth.Truth.assertThat +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class AndroidFileSizeFormatterTest { + @Test + fun `test api 24 long format`() { + val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.N) + assertThat(sut.format(1, useShortFormat = false)).isEqualTo("1.00B") + assertThat(sut.format(1000, useShortFormat = false)).isEqualTo("0.98KB") + assertThat(sut.format(1024, useShortFormat = false)).isEqualTo("1.00KB") + assertThat(sut.format(1024 * 1024, useShortFormat = false)).isEqualTo("1.00MB") + assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = false)).isEqualTo("1.00GB") + } + + @Test + fun `test api 26 long format`() { + val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.O) + assertThat(sut.format(1, useShortFormat = false)).isEqualTo("1.00B") + assertThat(sut.format(1000, useShortFormat = false)).isEqualTo("0.98KB") + assertThat(sut.format(1024 * 1024, useShortFormat = false)).isEqualTo("0.95MB") + assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = false)).isEqualTo("0.93GB") + } + + @Test + fun `test api 24 short format`() { + val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.N) + assertThat(sut.format(1, useShortFormat = true)).isEqualTo("1.0B") + assertThat(sut.format(1000, useShortFormat = true)).isEqualTo("0.98KB") + assertThat(sut.format(1024, useShortFormat = true)).isEqualTo("1.0KB") + assertThat(sut.format(1024 * 1024, useShortFormat = true)).isEqualTo("1.0MB") + assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = true)).isEqualTo("1.0GB") + } + + @Test + fun `test api 26 short format`() { + val sut = createAndroidFileSizeFormatter(sdkLevel = Build.VERSION_CODES.O) + assertThat(sut.format(1, useShortFormat = true)).isEqualTo("1.0B") + assertThat(sut.format(1000, useShortFormat = true)).isEqualTo("0.98KB") + assertThat(sut.format(1024 * 1024, useShortFormat = true)).isEqualTo("0.95MB") + assertThat(sut.format(1024 * 1024 * 1024, useShortFormat = true)).isEqualTo("0.93GB") + } + + private fun createAndroidFileSizeFormatter(sdkLevel: Int) = AndroidFileSizeFormatter( + context = RuntimeEnvironment.getApplication(), + sdkIntProvider = FakeBuildVersionSdkIntProvider(sdkInt = sdkLevel) + ) +} diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt index 6073b45351..0e6bf1505d 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt @@ -21,27 +21,36 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin -inline fun Node.createNode(context: BuildContext, plugins: List = emptyList()): NODE { +inline fun Node.createNode( + buildContext: BuildContext, + plugins: List = emptyList() +): N { val bindings: NodeFactoriesBindings = bindings() - return bindings.createNode(context, plugins) + return bindings.createNode(buildContext, plugins) } -inline fun Context.createNode(context: BuildContext, plugins: List = emptyList()): NODE { +inline fun Context.createNode( + buildContext: BuildContext, + plugins: List = emptyList() +): N { val bindings: NodeFactoriesBindings = bindings() - return bindings.createNode(context, plugins) + return bindings.createNode(buildContext, plugins) } -inline fun NodeFactoriesBindings.createNode(context: BuildContext, plugins: List = emptyList()): NODE { - val nodeClass = NODE::class.java +inline fun NodeFactoriesBindings.createNode( + buildContext: BuildContext, + plugins: List = emptyList() +): N { + val nodeClass = N::class.java val nodeFactoryMap = nodeFactories() // Note to developers: If you got the error below, make sure to build again after // clearing the cache (sometimes several times) to let Dagger generate the NodeFactory. val nodeFactory = nodeFactoryMap[nodeClass] ?: error("Cannot find NodeFactory for ${nodeClass.name}.") @Suppress("UNCHECKED_CAST") - val castedNodeFactory = nodeFactory as? AssistedNodeFactory - val node = castedNodeFactory?.create(context, plugins) - return node as NODE + val castedNodeFactory = nodeFactory as? AssistedNodeFactory + val node = castedNodeFactory?.create(buildContext, plugins) + return node as N } interface NodeFactoriesBindings { diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt index af2bca157b..6a97757b55 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt @@ -60,4 +60,11 @@ object MimeTypes { else -> OctetStream } } + + fun hasSubtype(mimeType: String): Boolean { + val components = mimeType.split("/") + if (components.size != 2) return false + val subType = components.last() + return subType.isNotBlank() && subType != "*" + } } 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 2dcb19cc43..9475275e5d 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -25,7 +25,6 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.SessionScope import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.impl.mode.RenderingMode -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem @@ -58,14 +57,13 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultRoomLastMessageFormatter @Inject constructor( private val sp: StringProvider, - private val matrixClient: MatrixClient, private val roomMembershipContentFormatter: RoomMembershipContentFormatter, private val profileChangeContentFormatter: ProfileChangeContentFormatter, private val stateContentFormatter: StateContentFormatter, ) : RoomLastMessageFormatter { override fun format(event: EventTimelineItem, isDmRoom: Boolean): CharSequence? { - val isOutgoing = matrixClient.isMe(event.sender) + val isOutgoing = event.isOwn val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value return when (val content = event.content) { is MessageContent -> processMessageContents(content, senderDisplayName, isDmRoom) diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt index 2b729fb0d5..ce4e4aa860 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultTimelineEventFormatter.kt @@ -21,7 +21,6 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.SessionScope import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.eventformatter.impl.mode.RenderingMode -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent @@ -42,7 +41,6 @@ import javax.inject.Inject @ContributesBinding(SessionScope::class) class DefaultTimelineEventFormatter @Inject constructor( private val sp: StringProvider, - private val matrixClient: MatrixClient, private val buildMeta: BuildMeta, private val roomMembershipContentFormatter: RoomMembershipContentFormatter, private val profileChangeContentFormatter: ProfileChangeContentFormatter, @@ -50,7 +48,7 @@ class DefaultTimelineEventFormatter @Inject constructor( ) : TimelineEventFormatter { override fun format(event: EventTimelineItem): CharSequence? { - val isOutgoing = matrixClient.isMe(event.sender) + val isOutgoing = event.isOwn val senderDisplayName = (event.senderProfile as? ProfileTimelineDetails.Ready)?.displayName ?: event.sender.value return when (val content = event.content) { is RoomMembershipContent -> { 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 8eed121568..a69704bbb4 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 @@ -75,7 +75,6 @@ class DefaultRoomLastMessageFormatterTest { val stringProvider = AndroidStringProvider(context.resources) formatter = DefaultRoomLastMessageFormatter( sp = AndroidStringProvider(context.resources), - matrixClient = fakeMatrixClient, roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider), profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider) @@ -798,6 +797,11 @@ class DefaultRoomLastMessageFormatterTest { private fun createRoomEvent(sentByYou: Boolean, senderDisplayName: String?, content: EventContent): EventTimelineItem { val sender = if (sentByYou) A_USER_ID else UserId("@someone_else:domain") val profile = ProfileTimelineDetails.Ready(senderDisplayName, false, null) - return anEventTimelineItem(content = content, senderProfile = profile, sender = sender) + return anEventTimelineItem( + content = content, + senderProfile = profile, + sender = sender, + isOwn = sentByYou, + ) } } 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 f9ac0d84a5..dc452f1514 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 @@ -41,7 +41,7 @@ interface MatrixClient : Closeable { val roomListService: RoomListService val mediaLoader: MatrixMediaLoader suspend fun getRoom(roomId: RoomId): MatrixRoom? - suspend fun findDM(userId: UserId): MatrixRoom? + suspend fun findDM(userId: UserId): RoomId? suspend fun ignoreUser(userId: UserId): Result suspend fun unignoreUser(userId: UserId): Result suspend fun createRoom(createRoomParams: CreateRoomParameters): 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 index 8b5d721b28..7e349216f4 100644 --- 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 @@ -16,14 +16,12 @@ package io.element.android.libraries.matrix.api.encryption +import androidx.compose.runtime.Immutable + +@Immutable sealed interface BackupUploadState { data object Unknown : BackupUploadState - data class CheckingIfUploadNeeded( - val backedUpCount: Int, - val totalCount: Int, - ) : BackupUploadState - data object Waiting : BackupUploadState data class Uploading( 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 index 1ee85a1cc1..ed45293c49 100644 --- 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 @@ -17,9 +17,10 @@ package io.element.android.libraries.matrix.api.encryption sealed interface EnableRecoveryProgress { - data object Unknown : EnableRecoveryProgress - data object CreatingRecoveryKey : EnableRecoveryProgress + data object Starting : EnableRecoveryProgress data object CreatingBackup : EnableRecoveryProgress + data object CreatingRecoveryKey : EnableRecoveryProgress data class BackingUp(val backedUpCount: Int, val totalCount: Int) : EnableRecoveryProgress + data object RoomKeyUploadError : EnableRecoveryProgress data class Done(val recoveryKey: String) : EnableRecoveryProgress } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/SteadyStateException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/SteadyStateException.kt index 1e83344a02..88525caac2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/SteadyStateException.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/SteadyStateException.kt @@ -16,6 +16,9 @@ package io.element.android.libraries.matrix.api.encryption +import androidx.compose.runtime.Immutable + +@Immutable sealed interface SteadyStateException { /** * The backup can be deleted. 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 059369c5da..d3ff0e9f83 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 @@ -16,9 +16,10 @@ package io.element.android.libraries.matrix.api.media -import java.time.Duration +import kotlinx.collections.immutable.ImmutableList +import kotlin.time.Duration data class AudioDetails( val duration: Duration, - val waveform: List, + val waveform: ImmutableList, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt index bd4539bced..542c13f75c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt @@ -16,7 +16,7 @@ package io.element.android.libraries.matrix.api.media -import java.time.Duration +import kotlin.time.Duration data class AudioInfo( val duration: Duration?, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt index b7af54c6b2..53cb2564e1 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt @@ -16,7 +16,7 @@ package io.element.android.libraries.matrix.api.media -import java.time.Duration +import kotlin.time.Duration data class VideoInfo( val duration: Duration?, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt index 72caa711cb..e1036011f6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkData.kt @@ -17,18 +17,21 @@ package io.element.android.libraries.matrix.api.permalink import android.net.Uri +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList /** * This sealed class represents all the permalink cases. * You don't have to instantiate yourself but should use [PermalinkParser] instead. */ +@Immutable sealed interface PermalinkData { data class RoomLink( val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?, - val viaParameters: List + val viaParameters: ImmutableList ) : PermalinkData /* diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt index 4a89d05276..dcd5221de8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.api.permalink import android.net.Uri import android.net.UrlQuerySanitizer import io.element.android.libraries.matrix.api.core.MatrixPatterns +import kotlinx.collections.immutable.toImmutableList import timber.log.Timber import java.net.URLDecoder @@ -80,7 +81,7 @@ object PermalinkParser { roomIdOrAlias = decodedIdentifier, isRoomAlias = true, eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, - viaParameters = viaQueryParameters + viaParameters = viaQueryParameters.toImmutableList() ) } else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier)) @@ -119,7 +120,7 @@ object PermalinkParser { roomIdOrAlias = identifier, isRoomAlias = false, eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, - viaParameters = viaQueryParameters + viaParameters = viaQueryParameters.toImmutableList() ) } } 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 19daf7d50d..d7c8d7f49c 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 @@ -132,6 +132,8 @@ interface MatrixRoom : Closeable { suspend fun canUserTriggerRoomNotification(userId: UserId): Result + suspend fun canUserJoinCall(userId: UserId): Result + suspend fun updateAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result @@ -238,5 +240,7 @@ interface MatrixRoom : Closeable { */ fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result + suspend fun pollHistory(): MatrixTimeline + override fun close() = destroy() } 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 index 6690387b04..1304ca40a8 100644 --- 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 @@ -16,8 +16,11 @@ package io.element.android.libraries.matrix.api.room +import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import kotlinx.collections.immutable.ImmutableList +@Immutable data class MatrixRoomInfo( val id: String, val name: String?, @@ -28,7 +31,7 @@ data class MatrixRoomInfo( val isSpace: Boolean, val isTombstoned: Boolean, val canonicalAlias: String?, - val alternativeAliases: List, + val alternativeAliases: ImmutableList, val currentUserMembership: CurrentUserMembership, val latestEvent: EventTimelineItem?, val inviter: RoomMember?, @@ -39,5 +42,5 @@ data class MatrixRoomInfo( val notificationCount: Long, val userDefinedNotificationMode: RoomNotificationMode?, val hasRoomCall: Boolean, - val activeRoomCallParticipants: List + val activeRoomCallParticipants: ImmutableList ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt index 5597aaf1c5..9a25aaa12e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomMembersState.kt @@ -17,13 +17,14 @@ package io.element.android.libraries.matrix.api.room import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList @Immutable sealed interface MatrixRoomMembersState { data object Unknown : MatrixRoomMembersState - data class Pending(val prevRoomMembers: List? = null) : MatrixRoomMembersState - data class Error(val failure: Throwable, val prevRoomMembers: List? = null) : MatrixRoomMembersState - data class Ready(val roomMembers: List) : MatrixRoomMembersState + data class Pending(val prevRoomMembers: ImmutableList? = null) : MatrixRoomMembersState + data class Error(val failure: Throwable, val prevRoomMembers: ImmutableList? = null) : MatrixRoomMembersState + data class Ready(val roomMembers: ImmutableList) : MatrixRoomMembersState } fun MatrixRoomMembersState.roomMembers(): List? { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.kt new file mode 100644 index 0000000000..7d0fd9a582 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StartDM.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.api.room + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId + +/** + * Try to find an existing DM with the given user, or create one if none exists. + */ +suspend fun MatrixClient.startDM(userId: UserId): StartDMResult { + val existingDM = findDM(userId) + return if (existingDM != null) { + StartDMResult.Success(existingDM, isNew = false) + } else { + createDM(userId).fold( + { StartDMResult.Success(it, isNew = true) }, + { StartDMResult.Failure(it) } + ) + } +} + +sealed interface StartDMResult { + data class Success(val roomId: RoomId, val isNew: Boolean) : StartDMResult + data class Failure(val throwable: Throwable) : StartDMResult +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt index 8248aaea90..1f46c2780d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt @@ -16,6 +16,12 @@ package io.element.android.libraries.matrix.api.roomlist +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + /** * RoomList with dynamic filtering and loading. * This is useful for large lists of rooms. @@ -23,17 +29,17 @@ package io.element.android.libraries.matrix.api.roomlist */ interface DynamicRoomList : RoomList { - companion object { - const val DEFAULT_PAGE_SIZE = 20 - const val DEFAULT_PAGES_TO_LOAD = 10 - } - sealed interface Filter { /** * No filter applied. */ data object All : Filter + /** + * Filter only the left rooms. + */ + data object AllNonLeft : Filter + /** * Filter all rooms. */ @@ -45,6 +51,10 @@ interface DynamicRoomList : RoomList { data class NormalizedMatchRoomName(val pattern: String) : Filter } + val currentFilter: StateFlow + val loadedPages: StateFlow + val pageSize: Int + /** * Load more rooms into the list if possible. */ @@ -61,3 +71,29 @@ interface DynamicRoomList : RoomList { */ suspend fun updateFilter(filter: Filter) } + +/** + * Offers a way to load all the rooms incrementally. + * It will load more room until all are loaded. + * If total number of rooms increase, it will load more pages if needed. + * The number of rooms is independent of the filter. + */ +fun DynamicRoomList.loadAllIncrementally(coroutineScope: CoroutineScope) { + combine( + loadedPages, + loadingState, + ) { loadedPages, loadingState -> + loadedPages to loadingState + } + .onEach { (loadedPages, loadingState) -> + when (loadingState) { + is RoomList.LoadingState.Loaded -> { + if (pageSize * loadedPages < loadingState.numberOfRooms) { + loadMore() + } + } + RoomList.LoadingState.NotLoaded -> Unit + } + } + .launchIn(coroutineScope) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt index 9897c2cff5..5682a43389 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListService.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.api.roomlist +import androidx.compose.runtime.Immutable import kotlinx.coroutines.flow.StateFlow /** @@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.StateFlow */ interface RoomListService { + @Immutable sealed interface State { data object Idle : State data object Running : State @@ -32,16 +34,17 @@ interface RoomListService { data object Terminated : State } + @Immutable sealed interface SyncIndicator { data object Show : SyncIndicator data object Hide : SyncIndicator } /** - * returns a [RoomList] object of all rooms we want to display. + * returns a [DynamicRoomList] object of all rooms we want to display. * This will exclude some rooms like the invites, or spaces. */ - val allRooms: RoomList + val allRooms: DynamicRoomList /** * returns a [RoomList] object of all invites. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt index 5fa55c357f..8dfa2e4819 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -interface MatrixTimeline { +interface MatrixTimeline: AutoCloseable { data class PaginationState( val isBackPaginating: Boolean, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index 71da78e369..5dcfd88016 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -16,11 +16,15 @@ package io.element.android.libraries.matrix.api.timeline.item.event +import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +@Immutable sealed interface EventContent data class MessageContent( @@ -43,8 +47,8 @@ data class PollContent( val question: String, val kind: PollKind, val maxSelections: ULong, - val answers: List, - val votes: Map>, + val answers: ImmutableList, + val votes: ImmutableMap>, val endTime: ULong?, val isEdited: Boolean, ) : EventContent @@ -52,6 +56,8 @@ data class PollContent( data class UnableToDecryptContent( val data: Data ) : EventContent { + + @Immutable sealed interface Data { data class OlmV1Curve25519AesSha2( val senderKey: String diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventReaction.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventReaction.kt index a2e68d17d2..65fc5a06a9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventReaction.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventReaction.kt @@ -16,7 +16,11 @@ package io.element.android.libraries.matrix.api.timeline.item.event +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable data class EventReaction( val key: String, - val senders: List + val senders: ImmutableList ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index 9f38ce9441..fa15f8f096 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import kotlinx.collections.immutable.ImmutableList data class EventTimelineItem( val eventId: EventId?, @@ -29,8 +30,8 @@ data class EventTimelineItem( val isOwn: Boolean, val isRemote: Boolean, val localSendState: LocalEventSendState?, - val reactions: List, - val receipts: List, + val reactions: ImmutableList, + val receipts: ImmutableList, val sender: UserId, val senderProfile: ProfileTimelineDetails, val timestamp: Long, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt index 14a84e2a90..6965ed9f1e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt @@ -16,9 +16,11 @@ package io.element.android.libraries.matrix.api.timeline.item.event +import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId +@Immutable sealed interface InReplyTo { /** The event details are not loaded yet. We can fetch them. */ data class NotLoaded(val eventId: EventId) : InReplyTo diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt index 265be8af79..f74ddca3a8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/LocalEventSendState.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.matrix.api.timeline.item.event +import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.EventId +@Immutable sealed interface LocalEventSendState { data object NotSentYet : LocalEventSendState data object Canceled : LocalEventSendState 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 14c2302def..0b2d948c4e 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 androidx.compose.runtime.Immutable 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 @@ -23,6 +24,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.VideoInfo +@Immutable sealed interface MessageType data class EmoteMessageType( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt index 2cbfaf76b4..6960b3565d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt @@ -16,6 +16,9 @@ package io.element.android.libraries.matrix.api.timeline.item.event +import androidx.compose.runtime.Immutable + +@Immutable sealed interface OtherState { data object PolicyRuleRoom : OtherState data object PolicyRuleServer : OtherState diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt index eddb9eb169..44664a256a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetails.kt @@ -16,6 +16,9 @@ package io.element.android.libraries.matrix.api.timeline.item.event +import androidx.compose.runtime.Immutable + +@Immutable sealed interface ProfileTimelineDetails { data object Unavailable : ProfileTimelineDetails diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixSearchUserResults.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixSearchUserResults.kt index a63a31b51f..a7985c864f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixSearchUserResults.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/user/MatrixSearchUserResults.kt @@ -16,7 +16,9 @@ package io.element.android.libraries.matrix.api.user +import kotlinx.collections.immutable.ImmutableList + data class MatrixSearchUserResults( - val results: List, + val results: ImmutableList, val limited: Boolean, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt index c463530050..639b704823 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt @@ -16,6 +16,8 @@ package io.element.android.libraries.matrix.api.verification +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -75,6 +77,7 @@ interface SessionVerificationService { } /** Verification status of the current session. */ +@Immutable sealed interface SessionVerifiedStatus { /** Unknown status, we couldn't read the actual value from the SDK. */ data object Unknown : SessionVerifiedStatus @@ -87,6 +90,7 @@ sealed interface SessionVerifiedStatus { } /** States produced by the [SessionVerificationService]. */ +@Immutable sealed interface VerificationFlowState { /** Initial state. */ data object Initial : VerificationFlowState @@ -98,7 +102,7 @@ sealed interface VerificationFlowState { data object StartedSasVerification : VerificationFlowState /** Verification data for the SAS verification (emojis) received. */ - data class ReceivedVerificationData(val emoji: List) : VerificationFlowState + data class ReceivedVerificationData(val emoji: ImmutableList) : VerificationFlowState /** Verification completed successfully. */ data object Finished : VerificationFlowState diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt index d55b6aebbe..590be150c4 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.api.permalink import com.google.common.truth.Truth.assertThat +import kotlinx.collections.immutable.persistentListOf import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -66,7 +67,7 @@ class PermalinkParserTest { roomIdOrAlias = "!aBCD1234:matrix.org", isRoomAlias = false, eventId = null, - viaParameters = emptyList(), + viaParameters = persistentListOf(), ) ) } @@ -79,7 +80,7 @@ class PermalinkParserTest { roomIdOrAlias = "!aBCD1234:matrix.org", isRoomAlias = false, eventId = "\$1234567890abcdef:matrix.org", - viaParameters = emptyList(), + viaParameters = persistentListOf(), ) ) } @@ -92,7 +93,7 @@ class PermalinkParserTest { roomIdOrAlias = "!aBCD1234:matrix.org", isRoomAlias = false, eventId = null, - viaParameters = emptyList(), + viaParameters = persistentListOf(), ) ) } @@ -105,7 +106,7 @@ class PermalinkParserTest { roomIdOrAlias = "!aBCD1234:matrix.org", isRoomAlias = false, eventId = "\$1234567890abcdef:matrix.org", - viaParameters = listOf("matrix.org", "matrix.com"), + viaParameters = persistentListOf("matrix.org", "matrix.com"), ) ) } @@ -118,7 +119,7 @@ class PermalinkParserTest { roomIdOrAlias = "#element-android:matrix.org", isRoomAlias = true, eventId = null, - viaParameters = emptyList(), + viaParameters = persistentListOf(), ) ) } diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 81b5f70a11..ef590c6889 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation("net.java.dev.jna:jna:5.13.0@aar") implementation(libs.androidx.datastore.preferences) implementation(libs.serialization.json) + implementation(libs.kotlinx.collections.immutable) testImplementation(libs.test.junit) testImplementation(libs.test.truth) 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 c277a01791..9c03bd7a0a 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 @@ -77,6 +77,7 @@ import org.matrix.rustcomponents.sdk.BackupState import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.NotificationProcessSetup +import org.matrix.rustcomponents.sdk.PowerLevels import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.TaskHandle @@ -214,6 +215,7 @@ class RustMatrixClient constructor( isKeyBackupEnabled = client.encryption().backupState() == BackupState.ENABLED, roomListItem = roomListItem, innerRoom = fullRoom, + innerTimeline = fullRoom.timeline(), roomNotificationSettingsService = notificationSettingsService, sessionCoroutineScope = sessionCoroutineScope, coroutineDispatchers = dispatchers, @@ -239,9 +241,8 @@ class RustMatrixClient constructor( } } - override suspend fun findDM(userId: UserId): MatrixRoom? { - val roomId = client.getDmRoom(userId.value)?.use { RoomId(it.id()) } - return roomId?.let { getRoom(it) } + override suspend fun findDM(userId: UserId): RoomId? { + return client.getDmRoom(userId.value)?.use { RoomId(it.id()) } } override suspend fun ignoreUser(userId: UserId): Result = withContext(sessionDispatcher) { @@ -274,6 +275,7 @@ class RustMatrixClient constructor( }, invite = createRoomParams.invite?.map { it.value }, avatar = createRoomParams.avatar, + powerLevelContentOverride = defaultRoomCreationPowerLevels, ) val roomId = RoomId(client.createRoom(rustParams)) @@ -296,7 +298,7 @@ class RustMatrixClient constructor( isDirect = true, visibility = RoomVisibility.PRIVATE, preset = RoomPreset.TRUSTED_PRIVATE_CHAT, - invite = listOf(userId) + invite = listOf(userId), ) return createRoom(createRoomParams) } @@ -481,3 +483,18 @@ class RustMatrixClient constructor( } } +private val defaultRoomCreationPowerLevels = PowerLevels( + usersDefault = null, + eventsDefault = null, + stateDefault = null, + ban = null, + kick = null, + redact = null, + invite = null, + notifications = null, + users = mapOf(), + events = mapOf( + "m.call.member" to 0, + "org.matrix.msc3401.call.member" to 0, + ) +) 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 index 9ac5330294..57a3914253 100644 --- 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 @@ -22,11 +22,6 @@ 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 -> 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 index a50a68267d..953c86d816 100644 --- 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 @@ -22,12 +22,14 @@ import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecover class EnableRecoveryProgressMapper { fun map(rustEnableProgress: RustEnableRecoveryProgress): EnableRecoveryProgress { return when (rustEnableProgress) { - is RustEnableRecoveryProgress.CreatingRecoveryKey -> EnableRecoveryProgress.CreatingRecoveryKey + is RustEnableRecoveryProgress.Starting -> EnableRecoveryProgress.Starting is RustEnableRecoveryProgress.CreatingBackup -> EnableRecoveryProgress.CreatingBackup + is RustEnableRecoveryProgress.CreatingRecoveryKey -> EnableRecoveryProgress.CreatingRecoveryKey is RustEnableRecoveryProgress.BackingUp -> EnableRecoveryProgress.BackingUp( backedUpCount = rustEnableProgress.backedUpCount.toInt(), totalCount = rustEnableProgress.totalCount.toInt(), ) + is RustEnableRecoveryProgress.RoomKeyUploadError -> EnableRecoveryProgress.RoomKeyUploadError is RustEnableRecoveryProgress.Done -> EnableRecoveryProgress.Done( recoveryKey = rustEnableProgress.recoveryKey ) 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 30b8c651a0..10929f0494 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 @@ -86,7 +86,7 @@ internal class RustEncryptionService( } }.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RecoveryState.WAITING_FOR_SYNC) - override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Unknown) + override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Starting) fun start() { service.backupStateListener(object : BackupStateListener { @@ -181,7 +181,7 @@ internal class RustEncryptionService( override suspend fun fixRecoveryIssues(recoveryKey: String): Result = withContext(dispatchers.io) { runCatching { - service.fixRecoveryIssues(recoveryKey) + service.recover(recoveryKey) } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/SteadyStateExceptionMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/SteadyStateExceptionMapper.kt index 331d3f8473..7439f016d8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/SteadyStateExceptionMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/SteadyStateExceptionMapper.kt @@ -23,13 +23,13 @@ class SteadyStateExceptionMapper { fun map(data: RustSteadyStateException): SteadyStateException { return when (data) { is RustSteadyStateException.BackupDisabled -> SteadyStateException.BackupDisabled( - message = data.message + message = data.message.orEmpty() ) is RustSteadyStateException.Connection -> SteadyStateException.Connection( - message = data.message + message = data.message.orEmpty() ) - is RustSteadyStateException.Laged -> SteadyStateException.Lagged( - message = data.message + is RustSteadyStateException.Lagged -> SteadyStateException.Lagged( + message = data.message.orEmpty() ) } } 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 5bca137a85..2bdcf6de0f 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 @@ -17,15 +17,18 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.matrix.api.media.AudioDetails +import kotlinx.collections.immutable.toImmutableList +import kotlin.time.toJavaDuration +import kotlin.time.toKotlinDuration import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent as RustAudioDetails fun RustAudioDetails.map(): AudioDetails = AudioDetails( - duration = duration, - waveform = waveform.fromMSC3246range(), + duration = duration.toKotlinDuration(), + waveform = waveform.fromMSC3246range().toImmutableList(), ) fun AudioDetails.map(): RustAudioDetails = RustAudioDetails( - duration = duration, + duration = duration.toJavaDuration(), waveform = waveform.toMSC3246range() ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt index 70c3bac6ed..b7cff173be 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt @@ -17,16 +17,18 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.matrix.api.media.AudioInfo +import kotlin.time.toJavaDuration +import kotlin.time.toKotlinDuration import org.matrix.rustcomponents.sdk.AudioInfo as RustAudioInfo fun RustAudioInfo.map(): AudioInfo = AudioInfo( - duration = duration, + duration = duration?.toKotlinDuration(), size = size?.toLong(), mimetype = mimetype ) fun AudioInfo.map(): RustAudioInfo = RustAudioInfo( - duration = duration, + duration = duration?.toJavaDuration(), size = size?.toULong(), mimetype = mimetype, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index fab0146f9d..6ae20e06af 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -82,7 +82,7 @@ class RustMediaLoader( val mediaFile = innerClient.getMediaFile( mediaSource = mediaSource, body = body, - mimeType = mimeType ?: MimeTypes.OctetStream, + mimeType = mimeType?.takeIf { MimeTypes.hasSubtype(it) } ?: MimeTypes.OctetStream, useCache = useCache, tempDir = cacheDirectory.path, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt index 661d1b9b33..bfdb328bc9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt @@ -17,10 +17,12 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.matrix.api.media.VideoInfo +import kotlin.time.toJavaDuration +import kotlin.time.toKotlinDuration import org.matrix.rustcomponents.sdk.VideoInfo as RustVideoInfo fun RustVideoInfo.map(): VideoInfo = VideoInfo( - duration = duration, + duration = duration?.toKotlinDuration(), height = height?.toLong(), width = width?.toLong(), mimetype = mimetype, @@ -31,7 +33,7 @@ fun RustVideoInfo.map(): VideoInfo = VideoInfo( ) fun VideoInfo.map(): RustVideoInfo = RustVideoInfo( - duration = duration, + duration = duration?.toJavaDuration(), height = height?.toULong(), width = width?.toULong(), mimetype = mimetype, 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 index c24d996714..3ea1895e22 100644 --- 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 @@ -20,6 +20,7 @@ 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 kotlinx.collections.immutable.toImmutableList import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.Membership as RustMembership import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo @@ -40,7 +41,7 @@ class MatrixRoomInfoMapper( isSpace = it.isSpace, isTombstoned = it.isTombstoned, canonicalAlias = it.canonicalAlias, - alternativeAliases = it.alternativeAliases, + alternativeAliases = it.alternativeAliases.toImmutableList(), currentUserMembership = it.membership.map(), latestEvent = it.latestEvent?.use (timelineItemMapper::map), inviter = it.inviter?.use(RoomMemberMapper::map), @@ -51,7 +52,7 @@ class MatrixRoomInfoMapper( notificationCount = it.notificationCount.toLong(), userDefinedNotificationMode = it.userDefinedNotificationMode?.map(), hasRoomCall = it.hasRoomCall, - activeRoomCallParticipants = it.activeRoomCallParticipants + activeRoomCallParticipants = it.activeRoomCallParticipants.toImmutableList() ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt index 2bfef368a9..f58a5c930d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt @@ -24,8 +24,8 @@ import io.element.android.libraries.matrix.impl.roomlist.roomOrNull import io.element.android.libraries.matrix.impl.timeline.runWithTimelineListenerRegistered import kotlinx.coroutines.CancellationException import kotlinx.coroutines.withTimeout -import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.Timeline import kotlin.time.Duration.Companion.milliseconds /** @@ -37,19 +37,19 @@ class RoomContentForwarder( ) { /** - * Forwards the event with the given [eventId] from the [fromRoom] to the given [toRoomIds]. - * @param fromRoom the room to forward the event from + * Forwards the event with the given [eventId] from the [fromTimeline] to the given [toRoomIds]. + * @param fromTimeline the room to forward the event from * @param eventId the id of the event to forward * @param toRoomIds the ids of the rooms to forward the event to * @param timeoutMs the maximum time in milliseconds to wait for the event to be sent to a room */ suspend fun forward( - fromRoom: Room, + fromTimeline: Timeline, eventId: EventId, toRoomIds: List, timeoutMs: Long = 5000L ) { - val content = fromRoom.getTimelineEventContentByEventId(eventId.value) + val content = fromTimeline.getTimelineEventContentByEventId(eventId.value) val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> roomListService.roomOrNull(roomId.value) } val targetRooms = targetSlidingSyncRooms.mapNotNull { slidingSyncRoom -> slidingSyncRoom.use { it.fullRoom() } } val failedForwardingTo = mutableSetOf() @@ -57,9 +57,9 @@ class RoomContentForwarder( room.use { targetRoom -> runCatching { // Sending a message requires a registered timeline listener - targetRoom.runWithTimelineListenerRegistered { + targetRoom.timeline().runWithTimelineListenerRegistered { withTimeout(timeoutMs.milliseconds) { - targetRoom.send(content) + targetRoom.timeline().send(content) } } } 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 40b0f08798..5bf2310048 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 @@ -57,6 +57,7 @@ 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.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -76,6 +77,7 @@ 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.Timeline import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml @@ -87,9 +89,10 @@ import java.io.File @OptIn(ExperimentalCoroutinesApi::class) class RustMatrixRoom( override val sessionId: SessionId, - isKeyBackupEnabled: Boolean, + private val isKeyBackupEnabled: Boolean, private val roomListItem: RoomListItem, private val innerRoom: Room, + private val innerTimeline: Timeline, private val roomNotificationSettingsService: RustNotificationSettingsService, sessionCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, @@ -130,7 +133,7 @@ class RustMatrixRoom( override val timeline = RustMatrixTimeline( isKeyBackupEnabled = isKeyBackupEnabled, matrixRoom = this, - innerRoom = innerRoom, + innerTimeline = innerTimeline, roomCoroutineScope = roomCoroutineScope, dispatcher = roomDispatcher, lastLoginTimestamp = sessionData.loginTimestamp, @@ -147,6 +150,7 @@ class RustMatrixRoom( override fun destroy() { roomCoroutineScope.cancel() + innerTimeline.destroy() innerRoom.destroy() roomListItem.destroy() specialModeEventTimelineItem?.destroy() @@ -195,7 +199,7 @@ class RustMatrixRoom( override suspend fun updateMembers(): Result = withContext(roomMembersDispatcher) { val currentState = _membersStateFlow.value - val currentMembers = currentState.roomMembers() + val currentMembers = currentState.roomMembers()?.toImmutableList() _membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers) var rustMembers: List? = null try { @@ -210,7 +214,7 @@ class RustMatrixRoom( } } val mappedMembers = rustMembers.parallelMap(RoomMemberMapper::map) - _membersStateFlow.value = MatrixRoomMembersState.Ready(mappedMembers) + _membersStateFlow.value = MatrixRoomMembersState.Ready(mappedMembers.toImmutableList()) Result.success(Unit) } catch (exception: CancellationException) { _membersStateFlow.value = MatrixRoomMembersState.Error(prevRoomMembers = currentMembers, failure = exception) @@ -254,7 +258,7 @@ class RustMatrixRoom( override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List): Result = withContext(roomDispatcher) { messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content -> runCatching { - innerRoom.send(content) + innerTimeline.send(content) } } } @@ -269,9 +273,9 @@ class RustMatrixRoom( withContext(roomDispatcher) { if (originalEventId != null) { runCatching { - val editedEvent = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(originalEventId.value) + val editedEvent = specialModeEventTimelineItem ?: innerTimeline.getEventTimelineItemByEventId(originalEventId.value) editedEvent.use { - innerRoom.edit( + innerTimeline.edit( newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), editItem = it, ) @@ -281,7 +285,7 @@ class RustMatrixRoom( } else { runCatching { transactionId?.let { cancelSend(it) } - innerRoom.send(messageEventContentFromParts(body, htmlBody)) + innerTimeline.send(messageEventContentFromParts(body, htmlBody)) } } } @@ -292,15 +296,15 @@ class RustMatrixRoom( runCatching { specialModeEventTimelineItem?.destroy() specialModeEventTimelineItem = null - specialModeEventTimelineItem = eventId?.let { innerRoom.getEventTimelineItemByEventId(it.value) } + specialModeEventTimelineItem = eventId?.let { innerTimeline.getEventTimelineItemByEventId(it.value) } } } override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List): Result = withContext(roomDispatcher) { runCatching { - val inReplyTo = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(eventId.value) + val inReplyTo = specialModeEventTimelineItem ?: innerTimeline.getEventTimelineItemByEventId(eventId.value) inReplyTo.use { eventTimelineItem -> - innerRoom.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem) + innerTimeline.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem) } specialModeEventTimelineItem = null } @@ -360,39 +364,45 @@ class RustMatrixRoom( } } + override suspend fun canUserJoinCall(userId: UserId): Result { + return runCatching { + innerRoom.canUserSendState(userId.value, StateEventType.ROOM_MEMBER_EVENT.map()) + } + } + 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()) + innerTimeline.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher()) } } override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result { return sendAttachment(listOf(file, thumbnailFile)) { - innerRoom.sendVideo(file.path, thumbnailFile.path, videoInfo.map(), progressCallback?.toProgressWatcher()) + innerTimeline.sendVideo(file.path, thumbnailFile.path, videoInfo.map(), progressCallback?.toProgressWatcher()) } } override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result { return sendAttachment(listOf(file)) { - innerRoom.sendAudio(file.path, audioInfo.map(), progressCallback?.toProgressWatcher()) + innerTimeline.sendAudio(file.path, audioInfo.map(), progressCallback?.toProgressWatcher()) } } override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result { return sendAttachment(listOf(file)) { - innerRoom.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher()) + innerTimeline.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher()) } } override suspend fun toggleReaction(emoji: String, eventId: EventId): Result = withContext(roomDispatcher) { runCatching { - innerRoom.toggleReaction(key = emoji, eventId = eventId.value) + innerTimeline.toggleReaction(key = emoji, eventId = eventId.value) } } override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(roomDispatcher) { runCatching { - roomContentForwarder.forward(fromRoom = innerRoom, eventId = eventId, toRoomIds = roomIds) + roomContentForwarder.forward(fromTimeline = innerTimeline, eventId = eventId, toRoomIds = roomIds) }.onFailure { Timber.e(it) } @@ -400,13 +410,13 @@ class RustMatrixRoom( override suspend fun retrySendMessage(transactionId: TransactionId): Result = withContext(roomDispatcher) { runCatching { - innerRoom.retrySend(transactionId.value) + innerTimeline.retrySend(transactionId.value) } } override suspend fun cancelSend(transactionId: TransactionId): Result = withContext(roomDispatcher) { runCatching { - innerRoom.cancelSend(transactionId.value) + innerTimeline.cancelSend(transactionId.value) } } @@ -451,7 +461,7 @@ class RustMatrixRoom( assetType: AssetType?, ): Result = withContext(roomDispatcher) { runCatching { - innerRoom.sendLocation( + innerTimeline.sendLocation( body = body, geoUri = geoUri, description = description, @@ -468,7 +478,7 @@ class RustMatrixRoom( pollKind: PollKind, ): Result = withContext(roomDispatcher) { runCatching { - innerRoom.createPoll( + innerTimeline.createPoll( question = question, answers = answers, maxSelections = maxSelections.toUByte(), @@ -486,11 +496,11 @@ class RustMatrixRoom( ): Result = withContext(roomDispatcher) { runCatching { val pollStartEvent = - innerRoom.getEventTimelineItemByEventId( + innerTimeline.getEventTimelineItemByEventId( eventId = pollStartId.value ) pollStartEvent.use { - innerRoom.editPoll( + innerTimeline.editPoll( question = question, answers = answers, maxSelections = maxSelections.toUByte(), @@ -506,7 +516,7 @@ class RustMatrixRoom( answers: List ): Result = withContext(roomDispatcher) { runCatching { - innerRoom.sendPollResponse( + innerTimeline.sendPollResponse( pollStartId = pollStartId.value, answers = answers, ) @@ -518,7 +528,7 @@ class RustMatrixRoom( text: String ): Result = withContext(roomDispatcher) { runCatching { - innerRoom.endPoll( + innerTimeline.endPoll( pollStartId = pollStartId.value, text = text, ) @@ -531,7 +541,7 @@ class RustMatrixRoom( waveform: List, progressCallback: ProgressCallback?, ): Result = sendAttachment(listOf(file)) { - innerRoom.sendVoiceMessage( + innerTimeline.sendVoiceMessage( url = file.path, audioInfo = audioInfo.map(), waveform = waveform.toMSC3246range(), @@ -560,7 +570,17 @@ class RustMatrixRoom( ) } - private suspend fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { + override suspend fun pollHistory() = RustMatrixTimeline( + isKeyBackupEnabled = isKeyBackupEnabled, + matrixRoom = this, + innerTimeline = innerRoom.pollHistory(), + roomCoroutineScope = roomCoroutineScope, + dispatcher = roomDispatcher, + lastLoginTimestamp = sessionData.loginTimestamp, + onNewSyncedEvent = { _syncUpdateFlow.value = systemClock.epochMillis() } + ) + + private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index 5afd7c4174..79ea6b17e0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -65,7 +65,6 @@ fun RoomListInterface.loadingStateFlow(): Flow = internal fun RoomListInterface.entriesFlow( pageSize: Int, - numberOfPages: Int, roomListDynamicEvents: Flow, initialFilterKind: RoomListEntriesDynamicFilterKind ): Flow> = @@ -84,9 +83,7 @@ internal fun RoomListInterface.entriesFlow( controller.setFilter(controllerEvents.filter) } is RoomListDynamicEvents.LoadMore -> { - repeat(numberOfPages) { - controller.addOnePage() - } + controller.addOnePage() } is RoomListDynamicEvents.Reset -> { controller.resetToOnePage() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt index b1ed372633..17bff4f5b7 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactory.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -39,57 +40,28 @@ internal class RoomListFactory( private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), ) { - /** - * Creates a room list that will load all rooms in a single page. - * It mimics the usage of the old api. - */ - fun createRoomList( - innerProvider: suspend () -> InnerRoomList, - ): RoomList { - return createRustRoomList( - pageSize = Int.MAX_VALUE, - numberOfPages = 1, - initialFilterKind = RoomListEntriesDynamicFilterKind.AllNonLeft, - innerRoomListProvider = innerProvider - ) - } - /** * Creates a room list that can be used to load more rooms and filter them dynamically. */ - fun createDynamicRoomList( - pageSize: Int = DynamicRoomList.DEFAULT_PAGE_SIZE, - pagesToLoad: Int = DynamicRoomList.DEFAULT_PAGES_TO_LOAD, - initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.None, + fun createRoomList( + pageSize: Int, + initialFilter: DynamicRoomList.Filter = DynamicRoomList.Filter.All, innerProvider: suspend () -> InnerRoomList ): DynamicRoomList { - return createRustRoomList( - pageSize = pageSize, - numberOfPages = pagesToLoad, - initialFilterKind = initialFilter.toRustFilter(), - innerRoomListProvider = innerProvider - ) - } - - private fun createRustRoomList( - pageSize: Int, - numberOfPages: Int, - initialFilterKind: RoomListEntriesDynamicFilterKind, - innerRoomListProvider: suspend () -> InnerRoomList - ): RustDynamicRoomList { val loadingStateFlow: MutableStateFlow = MutableStateFlow(RoomList.LoadingState.NotLoaded) val summariesFlow = MutableStateFlow>(emptyList()) val processor = RoomSummaryListProcessor(summariesFlow, innerRoomListService, dispatcher, roomSummaryDetailsFactory) - val dynamicEvents = MutableSharedFlow() - + // Makes sure we don't miss any events + val dynamicEvents = MutableSharedFlow(replay = 100) + val currentFilter = MutableStateFlow(initialFilter) + val loadedPages = MutableStateFlow(1) var innerRoomList: InnerRoomList? = null coroutineScope.launch(dispatcher) { - innerRoomList = innerRoomListProvider() + innerRoomList = innerProvider() innerRoomList?.let { innerRoomList -> innerRoomList.entriesFlow( pageSize = pageSize, - numberOfPages = numberOfPages, - initialFilterKind = initialFilterKind, + initialFilterKind = initialFilter.toRustFilter(), roomListDynamicEvents = dynamicEvents ).onEach { update -> processor.postUpdate(update) @@ -105,15 +77,26 @@ internal class RoomListFactory( }.invokeOnCompletion { innerRoomList?.destroy() } - return RustDynamicRoomList(summariesFlow, loadingStateFlow, dynamicEvents, processor) + return RustDynamicRoomList( + summaries = summariesFlow, + loadingState = loadingStateFlow, + currentFilter = currentFilter, + loadedPages = loadedPages, + dynamicEvents = dynamicEvents, + processor = processor, + pageSize = pageSize, + ) } } private class RustDynamicRoomList( override val summaries: MutableStateFlow>, override val loadingState: MutableStateFlow, + override val currentFilter: MutableStateFlow, + override val loadedPages: MutableStateFlow, private val dynamicEvents: MutableSharedFlow, private val processor: RoomSummaryListProcessor, + override val pageSize: Int, ) : DynamicRoomList { override suspend fun rebuildSummaries() { @@ -121,16 +104,19 @@ private class RustDynamicRoomList( } override suspend fun updateFilter(filter: DynamicRoomList.Filter) { + currentFilter.emit(filter) val filterEvent = RoomListDynamicEvents.SetFilter(filter.toRustFilter()) dynamicEvents.emit(filterEvent) } override suspend fun loadMore() { dynamicEvents.emit(RoomListDynamicEvents.LoadMore) + loadedPages.getAndUpdate { it + 1 } } override suspend fun reset() { dynamicEvents.emit(RoomListDynamicEvents.Reset) + loadedPages.emit(1) } } @@ -146,6 +132,7 @@ private fun DynamicRoomList.Filter.toRustFilter(): RoomListEntriesDynamicFilterK DynamicRoomList.Filter.All -> RoomListEntriesDynamicFilterKind.All is DynamicRoomList.Filter.NormalizedMatchRoomName -> RoomListEntriesDynamicFilterKind.NormalizedMatchRoomName(this.pattern) DynamicRoomList.Filter.None -> RoomListEntriesDynamicFilterKind.None + DynamicRoomList.Filter.AllNonLeft -> RoomListEntriesDynamicFilterKind.AllNonLeft } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt index 6ae677831a..3dce8a0fc4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListService.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.matrix.impl.roomlist +import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -34,20 +36,31 @@ import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator import timber.log.Timber import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService +private const val DEFAULT_PAGE_SIZE = 20 + internal class RustRoomListService( private val innerRoomListService: InnerRustRoomListService, private val sessionCoroutineScope: CoroutineScope, roomListFactory: RoomListFactory, ) : RoomListService { - override val allRooms: RoomList = roomListFactory.createRoomList { + override val allRooms: DynamicRoomList = roomListFactory.createRoomList( + pageSize = DEFAULT_PAGE_SIZE, + initialFilter = DynamicRoomList.Filter.AllNonLeft, + ) { innerRoomListService.allRooms() } - override val invites: RoomList = roomListFactory.createRoomList { + override val invites: RoomList = roomListFactory.createRoomList( + pageSize = Int.MAX_VALUE, + ) { innerRoomListService.invites() } + init { + allRooms.loadAllIncrementally(sessionCoroutineScope) + } + override fun updateAllRoomsVisibleRange(range: IntRange) { Timber.v("setVisibleRange=$range") sessionCoroutineScope.launch { 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 e87ae74f30..8eb2f999d1 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 @@ -29,29 +29,28 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.catch import org.matrix.rustcomponents.sdk.BackPaginationStatus import org.matrix.rustcomponents.sdk.BackPaginationStatusListener -import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.Timeline import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineListener import timber.log.Timber -internal fun Room.timelineDiffFlow(onInitialList: suspend (List) -> Unit): Flow> = +internal fun Timeline.timelineDiffFlow(onInitialList: suspend (List) -> Unit): Flow> = callbackFlow { val listener = object : TimelineListener { override fun onUpdate(diff: List) { trySendBlocking(diff) } } - val roomId = id() - Timber.d("Open timelineDiffFlow for room $roomId") - val result = addTimelineListener(listener) + Timber.d("Open timelineDiffFlow for TimelineInterface ${this@timelineDiffFlow}") + val result = addListener(listener) try { onInitialList(result.items) } catch (exception: Exception) { - Timber.d(exception, "Catch failure in timelineDiffFlow of room $roomId") + Timber.d(exception, "Catch failure in timelineDiffFlow of TimelineInterface ${this@timelineDiffFlow}") } awaitClose { - Timber.d("Close timelineDiffFlow for room $roomId") + Timber.d("Close timelineDiffFlow for TimelineInterface ${this@timelineDiffFlow}") result.itemsStream.cancelAndDestroy() result.items.destroyAll() } @@ -59,7 +58,7 @@ internal fun Room.timelineDiffFlow(onInitialList: suspend (List) - Timber.d(it, "timelineDiffFlow() failed") }.buffer(Channel.UNLIMITED) -internal fun Room.backPaginationStatusFlow(): Flow = +internal fun Timeline.backPaginationStatusFlow(): Flow = mxCallbackFlow { val listener = object : BackPaginationStatusListener { override fun onUpdate(status: BackPaginationStatus) { @@ -71,8 +70,8 @@ internal fun Room.backPaginationStatusFlow(): Flow = } }.buffer(Channel.UNLIMITED) -internal suspend fun Room.runWithTimelineListenerRegistered(action: suspend () -> Unit) { - val result = addTimelineListener(NoOpTimelineListener) +internal suspend fun Timeline.runWithTimelineListenerRegistered(action: suspend () -> Unit) { + val result = addListener(NoOpTimelineListener) try { action() } finally { 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 85e6905f0a..85f59f9e57 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 @@ -46,7 +46,7 @@ import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.BackPaginationStatus import org.matrix.rustcomponents.sdk.EventItemOrigin import org.matrix.rustcomponents.sdk.PaginationOptions -import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.Timeline import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineItem import timber.log.Timber @@ -59,9 +59,9 @@ class RustMatrixTimeline( roomCoroutineScope: CoroutineScope, isKeyBackupEnabled: Boolean, private val matrixRoom: MatrixRoom, - private val innerRoom: Room, + private val innerTimeline: Timeline, private val dispatcher: CoroutineDispatcher, - private val lastLoginTimestamp: Date?, + lastLoginTimestamp: Date?, private val onNewSyncedEvent: () -> Unit, ) : MatrixTimeline { @@ -109,7 +109,7 @@ class RustMatrixTimeline( Timber.d("Initialize timeline for room ${matrixRoom.roomId}") roomCoroutineScope.launch(dispatcher) { - innerRoom.timelineDiffFlow { initialList -> + innerTimeline.timelineDiffFlow { initialList -> postItems(initialList) }.onEach { diffs -> if (diffs.any { diff -> diff.eventOrigin() == EventItemOrigin.SYNC }) { @@ -118,7 +118,7 @@ class RustMatrixTimeline( postDiffs(diffs) }.launchIn(this) - innerRoom.backPaginationStatusFlow() + innerTimeline.backPaginationStatusFlow() .onEach { postPaginationStatus(it) } @@ -130,7 +130,7 @@ class RustMatrixTimeline( private suspend fun fetchMembers() = withContext(dispatcher) { initLatch.await() - innerRoom.fetchMembers() + innerTimeline.fetchMembers() } @OptIn(ExperimentalCoroutinesApi::class) @@ -188,7 +188,7 @@ class RustMatrixTimeline( override suspend fun fetchDetailsForEvent(eventId: EventId): Result = withContext(dispatcher) { runCatching { - innerRoom.fetchDetailsForEvent(eventId.value) + innerTimeline.fetchDetailsForEvent(eventId.value) } } @@ -201,7 +201,7 @@ class RustMatrixTimeline( items = untilNumberOfItems.toUShort(), waitForToken = true, ) - innerRoom.paginateBackwards(paginationOptions) + innerTimeline.paginateBackwards(paginationOptions) }.onFailure { error -> if (error is TimelineException.CannotPaginate) { Timber.d("Can't paginate backwards on room ${matrixRoom.roomId}, we're already at the start") @@ -219,10 +219,14 @@ class RustMatrixTimeline( override suspend fun sendReadReceipt(eventId: EventId) = withContext(dispatcher) { runCatching { - innerRoom.sendReadReceipt(eventId = eventId.value) + innerTimeline.sendReadReceipt(eventId = eventId.value) } } + override fun close() { + innerTimeline.close() + } + fun getItemById(eventId: EventId): MatrixTimelineItem.Event? { return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index d761e91d6c..4e965fc4ef 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -27,6 +27,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimeli import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.EventItemOrigin as RustEventItemOrigin import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState @@ -81,7 +84,7 @@ fun RustEventSendState?.map(): LocalEventSendState? { } } -private fun List?.map(): List { +private fun List?.map(): ImmutableList { return this?.map { EventReaction( key = it.key, @@ -90,18 +93,20 @@ private fun List?.map(): List { senderId = UserId(sender.senderId), timestamp = sender.timestamp.toLong() ) - } + }.toImmutableList() ) - } ?: emptyList() + }?.toImmutableList() ?: persistentListOf() } -private fun Map.map(): List { +private fun Map.map(): ImmutableList { return map { - Receipt( - userId = UserId(it.key), - timestamp = it.value.timestamp?.toLong() ?: 0 - ) - }.sortedByDescending { it.timestamp } + Receipt( + userId = UserId(it.key), + timestamp = it.value.timestamp?.toLong() ?: 0 + ) + } + .sortedByDescending { it.timestamp } + .toImmutableList() } private fun RustEventTimelineItemDebugInfo.map(): TimelineItemDebugInfo { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt index 074bb38e05..ee28b18d08 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt @@ -32,6 +32,8 @@ 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.impl.media.map import io.element.android.libraries.matrix.impl.poll.map +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import org.matrix.rustcomponents.sdk.TimelineItemContent import org.matrix.rustcomponents.sdk.TimelineItemContentKind import org.matrix.rustcomponents.sdk.use @@ -106,10 +108,10 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap question = kind.question, kind = kind.kind.map(), maxSelections = kind.maxSelections, - answers = kind.answers.map { answer -> answer.map() }, + answers = kind.answers.map { answer -> answer.map() }.toImmutableList(), votes = kind.votes.mapValues { vote -> - vote.value.map { userId -> UserId(userId) } - }, + vote.value.map { userId -> UserId(userId) }.toImmutableList() + }.toImmutableMap(), endTime = kind.endTime, isEdited = kind.hasBeenEdited, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapper.kt index 1ec0b512ec..b95cbaeed1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/usersearch/UserSearchResultMapper.kt @@ -17,13 +17,14 @@ package io.element.android.libraries.matrix.impl.usersearch import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults +import kotlinx.collections.immutable.toImmutableList import org.matrix.rustcomponents.sdk.SearchUsersResults object UserSearchResultMapper { fun map(result: SearchUsersResults): MatrixSearchUserResults { return MatrixSearchUserResults( - results = result.results.map(UserProfileMapper::map), + results = result.results.map(UserProfileMapper::map).toImmutableList(), limited = result.limited, ) } 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 a4dfadfa5b..3d00c2d50e 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,6 +23,7 @@ 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.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -106,8 +107,9 @@ class RustSessionVerificationService( override fun didReceiveVerificationData(data: List) { val emojis = data.map { emoji -> - emoji.use { VerificationEmoji(it.symbol(), it.description()) } - } + emoji.use { VerificationEmoji(it.symbol(), it.description()) } + } + .toImmutableList() _verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(emojis) } diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index 4e8893aab6..9c41948bd7 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -28,4 +28,5 @@ dependencies { api(libs.coroutines.core) implementation(libs.coroutines.test) implementation(projects.tests.testutils) + implementation(libs.kotlinx.collections.immutable) } 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 e8228e806e..3977fbb133 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 @@ -39,7 +39,6 @@ import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.pushers.FakePushersService -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService @@ -72,8 +71,7 @@ class FakeMatrixClient( private var unignoreUserResult: Result = Result.success(Unit) private var createRoomResult: Result = Result.success(A_ROOM_ID) private var createDmResult: Result = Result.success(A_ROOM_ID) - private var createDmFailure: Throwable? = null - private var findDmResult: MatrixRoom? = FakeMatrixRoom() + private var findDmResult: RoomId? = A_ROOM_ID private var logoutFailure: Throwable? = null private val getRoomResults = mutableMapOf() private val searchUserResults = mutableMapOf>() @@ -87,7 +85,7 @@ class FakeMatrixClient( return getRoomResults[roomId] } - override suspend fun findDM(userId: UserId): MatrixRoom? { + override suspend fun findDM(userId: UserId): RoomId? { return findDmResult } @@ -99,14 +97,11 @@ class FakeMatrixClient( return unignoreUserResult } - override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result { - delay(100) + override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = simulateLongTask { return createRoomResult } - override suspend fun createDM(userId: UserId): Result { - delay(100) - createDmFailure?.let { throw it } + override suspend fun createDM(userId: UserId): Result = simulateLongTask { return createDmResult } @@ -206,11 +201,7 @@ class FakeMatrixClient( unignoreUserResult = result } - fun givenCreateDmError(failure: Throwable?) { - createDmFailure = failure - } - - fun givenFindDmResult(result: MatrixRoom?) { + fun givenFindDmResult(result: RoomId?) { findDmResult = result } 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 9f10f4ba35..f8a2d411fe 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 @@ -30,7 +30,7 @@ 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 enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Starting) private var waitForBackupUploadSteadyStateFlow: Flow = flowOf() private var fixRecoveryIssuesFailure: Exception? = null 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 531c792713..f16fdfbe98 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 @@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific 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.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -110,6 +111,7 @@ class FakeMatrixRoom( private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io") private var getWidgetDriverResult: Result = Result.success(FakeWidgetDriver()) private var canUserTriggerRoomNotificationResult: Result = Result.success(true) + private var canUserJoinCallResult: Result = Result.success(true) var sendMessageMentions = emptyList() val editMessageCalls = mutableListOf>() @@ -291,6 +293,10 @@ class FakeMatrixRoom( return canUserTriggerRoomNotificationResult } + override suspend fun canUserJoinCall(userId: UserId): Result { + return canUserJoinCallResult + } + override suspend fun sendImage( file: File, thumbnailFile: File, @@ -425,6 +431,9 @@ class FakeMatrixRoom( ): Result = generateWidgetWebViewUrlResult override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = getWidgetDriverResult + override suspend fun pollHistory(): MatrixTimeline { + return FakeMatrixTimeline() + } fun givenLeaveRoomError(throwable: Throwable?) { this.leaveRoomError = throwable @@ -470,6 +479,10 @@ class FakeMatrixRoom( canUserTriggerRoomNotificationResult = result } + fun givenCanUserJoinCall(result: Result) { + canUserJoinCallResult = result + } + fun givenIgnoreResult(result: Result) { ignoreResult = result } @@ -612,7 +625,7 @@ fun aRoomInfo( isSpace = isSpace, isTombstoned = isTombstoned, canonicalAlias = canonicalAlias, - alternativeAliases = alternativeAliases, + alternativeAliases = alternativeAliases.toImmutableList(), currentUserMembership = currentUserMembership, latestEvent = latestEvent, inviter = inviter, @@ -623,5 +636,5 @@ fun aRoomInfo( notificationCount = notificationCount, userDefinedNotificationMode = userDefinedNotificationMode, hasRoomCall = hasRoomCall, - activeRoomCallParticipants = activeRoomCallParticipants + activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(), ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 645aac488c..4dde81813d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -44,6 +44,9 @@ 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_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf fun aRoomSummaryFilled( roomId: RoomId = A_ROOM_ID, @@ -107,8 +110,8 @@ fun anEventTimelineItem( isOwn: Boolean = false, isRemote: Boolean = false, localSendState: LocalEventSendState? = null, - reactions: List = emptyList(), - receipts: List = emptyList(), + reactions: ImmutableList = persistentListOf(), + receipts: ImmutableList = persistentListOf(), sender: UserId = A_USER_ID, senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(), timestamp: Long = 0L, @@ -181,13 +184,13 @@ fun aTimelineItemDebugInfo( fun aPollContent( question: String = "Do you like polls?", - answers: List = listOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")), + answers: ImmutableList = persistentListOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")), ) = PollContent( question = question, kind = PollKind.Disclosed, maxSelections = 1u, answers = answers, - votes = mapOf(), + votes = persistentMapOf(), endTime = null, isEdited = false, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt index ece77d53b5..5c7d0983cd 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/FakeRoomListService.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.test.roomlist +import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomSummary @@ -54,14 +55,16 @@ class FakeRoomListService : RoomListService { var latestSlidingSyncRange: IntRange? = null private set - override val allRooms: RoomList = SimplePagedRoomList( + override val allRooms: DynamicRoomList = SimplePagedRoomList( allRoomSummariesFlow, allRoomsLoadingStateFlow, + MutableStateFlow(DynamicRoomList.Filter.None) ) override val invites: RoomList = SimplePagedRoomList( inviteRoomSummariesFlow, inviteRoomsLoadingStateFlow, + MutableStateFlow(DynamicRoomList.Filter.None) ) override fun updateAllRoomsVisibleRange(range: IntRange) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt index 2555fe937d..e94002bd1d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/roomlist/SimplePagedRoomList.kt @@ -19,23 +19,30 @@ package io.element.android.libraries.matrix.test.roomlist import io.element.android.libraries.matrix.api.roomlist.DynamicRoomList import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomSummary +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.getAndUpdate data class SimplePagedRoomList( override val summaries: StateFlow>, - override val loadingState: StateFlow + override val loadingState: StateFlow, + override val currentFilter: MutableStateFlow ) : DynamicRoomList { + override val pageSize: Int = Int.MAX_VALUE + override val loadedPages = MutableStateFlow(1) + override suspend fun loadMore() { //No-op + loadedPages.getAndUpdate { it + 1 } } override suspend fun reset() { - //No-op + loadedPages.emit(1) } override suspend fun updateFilter(filter: DynamicRoomList.Filter) { - //No-op + currentFilter.emit(filter) } override suspend fun rebuildSummaries() { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt index 6cf6f05cbe..21325d7a83 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt @@ -79,4 +79,6 @@ class FakeMatrixTimeline( sendReadReceiptLatch?.complete(Unit) Result.success(Unit) } + + override fun close() = Unit } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt index 62b0b39adf..34405f1e3d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt @@ -20,6 +20,8 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationEmoji +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -29,7 +31,7 @@ class FakeSessionVerificationService : SessionVerificationService { private val _sessionVerifiedStatus = MutableStateFlow(SessionVerifiedStatus.Unknown) private var _verificationFlowState = MutableStateFlow(VerificationFlowState.Initial) private var _canVerifySessionFlow = MutableStateFlow(true) - private var emojiList = emptyList() + private var emojiList = persistentListOf() var shouldFail = false override val verificationFlowState: StateFlow =_verificationFlowState @@ -87,7 +89,7 @@ class FakeSessionVerificationService : SessionVerificationService { } fun givenEmojiList(emojis: List) { - this.emojiList = emojis + this.emojiList = emojis.toPersistentList() } override suspend fun reset() { diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt index 31c6a813ff..9a48b50aca 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt @@ -31,6 +31,6 @@ interface MediaPreProcessor { compressIfPossible: Boolean ): Result - data class Failure(override val cause: Throwable?) : RuntimeException(cause) + data class Failure(override val cause: Throwable?) : Exception(cause) } diff --git a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTests.kt b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTests.kt index 480cf8065f..35aee8b2d5 100644 --- a/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTests.kt +++ b/libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTests.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.mediaupload.api import android.net.Uri import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -39,7 +40,7 @@ class MediaSenderTests { val sender = aMediaSender(preProcessor) val uri = Uri.parse("content://image.jpg") - sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true) + sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true) assertThat(preProcessor.processCallCount).isEqualTo(1) } @@ -50,7 +51,7 @@ class MediaSenderTests { val sender = aMediaSender(room = room) val uri = Uri.parse("content://image.jpg") - sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true) + sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true) assertThat(room.sendMediaCount).isEqualTo(1) } @@ -63,7 +64,7 @@ class MediaSenderTests { val sender = aMediaSender(preProcessor) val uri = Uri.parse("content://image.jpg") - val result = sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true) + val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true) assertThat(result.exceptionOrNull()).isNotNull() } @@ -76,7 +77,7 @@ class MediaSenderTests { val sender = aMediaSender(room = room) val uri = Uri.parse("content://image.jpg") - val result = sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true) + val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true) assertThat(result.exceptionOrNull()).isNotNull() } @@ -88,7 +89,7 @@ class MediaSenderTests { val sender = aMediaSender(room = room) val sendJob = launch { val uri = Uri.parse("content://image.jpg") - sender.sendMedia(uri = uri, mimeType = "image/jpeg", compressIfPossible = true) + sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true) } // Wait until several internal tasks run and the file is being uploaded advanceTimeBy(3L) diff --git a/libraries/mediaupload/impl/build.gradle.kts b/libraries/mediaupload/impl/build.gradle.kts index a23ab14b74..c65b6b01a6 100644 --- a/libraries/mediaupload/impl/build.gradle.kts +++ b/libraries/mediaupload/impl/build.gradle.kts @@ -27,6 +27,12 @@ android { generateDaggerFactories.set(true) } + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) @@ -37,6 +43,7 @@ android { implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) + implementation(projects.services.toolbox.api) implementation(libs.inject) implementation(libs.androidx.exifinterface) implementation(libs.coroutines.core) @@ -44,7 +51,10 @@ android { implementation(libs.vanniktech.blurhash) testImplementation(libs.test.junit) + testImplementation(libs.test.robolectric) testImplementation(libs.coroutines.test) testImplementation(libs.test.truth) + testImplementation(projects.tests.testutils) + testImplementation(projects.services.toolbox.test) } } 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 205be3b241..f05fe5bdc3 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 @@ -47,8 +47,9 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import java.io.File import java.io.InputStream -import java.time.Duration import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds @ContributesBinding(AppScope::class) class AndroidMediaPreProcessor @Inject constructor( @@ -269,6 +270,6 @@ fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailResult: Thumbn private fun MediaMetadataRetriever.extractDuration(): Duration { val durationInMs = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L - return Duration.ofMillis(durationInMs) + return durationInMs.milliseconds } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt index d936b3a5bf..9f93818576 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt @@ -24,8 +24,8 @@ import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize import io.element.android.libraries.androidutils.bitmap.resizeToMax import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation import io.element.android.libraries.androidutils.file.createTmpFile +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.ApplicationContext -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File import java.io.InputStream @@ -33,8 +33,8 @@ import javax.inject.Inject class ImageCompressor @Inject constructor( @ApplicationContext private val context: Context, + private val dispatchers: CoroutineDispatchers, ) { - /** * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode], then writes it into a * temporary file using the passed [format], [orientation] and [desiredQuality]. @@ -46,7 +46,7 @@ class ImageCompressor @Inject constructor( format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, orientation: Int = ExifInterface.ORIENTATION_UNDEFINED, desiredQuality: Int = 80, - ): Result = withContext(Dispatchers.IO) { + ): Result = withContext(dispatchers.io) { runCatching { val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow() // Encode bitmap to the destination temporary file diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt index 2cee2566d1..40a75b6428 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.ThumbnailInfo +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import kotlinx.coroutines.suspendCancellableCoroutine import java.io.File import javax.inject.Inject @@ -56,13 +57,14 @@ private const val VIDEO_THUMB_FRAME = 0L class ThumbnailFactory @Inject constructor( @ApplicationContext private val context: Context, + private val sdkIntProvider: BuildVersionSdkIntProvider ) { @SuppressLint("NewApi") suspend fun createImageThumbnail(file: File): ThumbnailResult { return createThumbnail { cancellationSignal -> // This API works correctly with GIF - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.Q)) { ThumbnailUtils.createImageThumbnail( file, Size(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT), diff --git a/libraries/mediaupload/impl/src/test/assets/animated_gif.gif b/libraries/mediaupload/impl/src/test/assets/animated_gif.gif new file mode 100644 index 0000000000..8fd2ce1550 --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/animated_gif.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6831610b21668c49e31732f9005177e959277233d3cab758910e061294f91d79 +size 687979 diff --git a/libraries/mediaupload/impl/src/test/assets/image.png b/libraries/mediaupload/impl/src/test/assets/image.png new file mode 100644 index 0000000000..b0946f7ba8 --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a980f7b74cb9edc323919db8652798da4b3dcf865fc7b6a1eb1110096b7bfb4f +size 1856786 diff --git a/libraries/mediaupload/impl/src/test/assets/sample3s.mp3 b/libraries/mediaupload/impl/src/test/assets/sample3s.mp3 new file mode 100644 index 0000000000..cb4db9c9d8 --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/sample3s.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0244590f2b4bcb62352b574e78bea940e8d89cfa69823b5208ef4c43e0abcb44 +size 52079 diff --git a/libraries/mediaupload/impl/src/test/assets/text.txt b/libraries/mediaupload/impl/src/test/assets/text.txt new file mode 100644 index 0000000000..d45ec4338b --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/text.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ba904eae8773b70c75333db4de2f3ac45a8ad4ddba1b242f0b3cfc199391dd8 +size 13 diff --git a/libraries/mediaupload/impl/src/test/assets/video.mp4 b/libraries/mediaupload/impl/src/test/assets/video.mp4 new file mode 100644 index 0000000000..4d57318b1d --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/video.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb58436524db95bd0c10b2c3023c2eb7b87404a2eab8987939f051647eb859d3 +size 1673712 diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessorTest.kt new file mode 100644 index 0000000000..3d745b3ec3 --- /dev/null +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessorTest.kt @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mediaupload + +import android.content.Context +import android.os.Build +import androidx.core.net.toUri +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.mimetype.MimeTypes +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 +import io.element.android.libraries.matrix.api.media.ThumbnailInfo +import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.mediaupload.api.MediaUploadInfo +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import kotlin.time.Duration + +@RunWith(RobolectricTestRunner::class) +class AndroidMediaPreProcessorTest { + @Test + fun `test processing image`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "image.png") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Png, + deleteOriginal = false, + compressIfPossible = true, + ) + // This is failing for now + val error = result.exceptionOrNull() + assertThat(error).isInstanceOf(MediaPreProcessor.Failure::class.java) + assertThat(error?.cause).isInstanceOf(NullPointerException::class.java) + /* + val data = result.getOrThrow() + assertThat(data.file.path).endsWith("image.png") + val info = data as MediaUploadInfo.Image + assertThat(info.thumbnailFile).isNull() // TODO Check this + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Png, + size = 114_867, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(file.exists()).isTrue() + */ + } + + @Test + fun `test processing image api Q`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context, sdkIntVersion = Build.VERSION_CODES.Q) + val file = getFileFromAssets(context, "image.png") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Png, + deleteOriginal = false, + compressIfPossible = true, + ) + // This is not working for now + val error = result.exceptionOrNull() + assertThat(error).isInstanceOf(MediaPreProcessor.Failure::class.java) + assertThat(error?.cause).isInstanceOf(NoSuchMethodError::class.java) + /* + val data = result.getOrThrow() + assertThat(data.file.path).endsWith("image.png") + val info = data as MediaUploadInfo.Image + assertThat(info.thumbnailFile).isNull() // TODO Check this + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Png, + size = 114_867, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(file.exists()).isTrue() + */ + } + + @Test + fun `test processing image no compression`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "image.png") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Png, + deleteOriginal = false, + compressIfPossible = false, + ).getOrThrow() + assertThat(result.file.path).endsWith("image.png") + val info = result as MediaUploadInfo.Image + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Png, + size = 1_856_786, + thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643), + thumbnailSource = null, + blurhash = "K00000fQfQfQfQfQfQfQfQ", + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing image and delete`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "image.png") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Png, + deleteOriginal = true, + compressIfPossible = false, + ).getOrThrow() + assertThat(result.file.path).endsWith("image.png") + val info = result as MediaUploadInfo.Image + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Png, + size = 1_856_786, + thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643), + thumbnailSource = null, + blurhash = "K00000fQfQfQfQfQfQfQfQ", + ) + ) + // Does not work + // assertThat(file.exists()).isFalse() + } + + @Test + fun `test processing gif`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "animated_gif.gif") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Gif, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("animated_gif.gif") + val info = result as MediaUploadInfo.Image + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 600, + width = 800, + mimetype = MimeTypes.Gif, + size = 687_979, + thumbnailInfo = ThumbnailInfo(height = 50, width = 50, mimetype = MimeTypes.Jpeg, size = 691), + thumbnailSource = null, + blurhash = "K00000fQfQfQfQfQfQfQfQ", + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing file`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "text.txt") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.PlainText, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("text.txt") + val info = result as MediaUploadInfo.AnyFile + assertThat(info.fileInfo).isEqualTo( + FileInfo( + mimetype = MimeTypes.PlainText, + size = 13, + thumbnailInfo = null, + thumbnailSource = null, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Ignore("Compressing video is not working with Robolectric") + @Test + fun `test processing video`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "video.mp4") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Mp4, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("video.mp4") + val info = result as MediaUploadInfo.Video + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.videoInfo).isEqualTo( + VideoInfo( + duration = Duration.ZERO, // Not available with Robolectric? + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Mp4, + size = 114_867, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing video no compression`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "video.mp4") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Mp4, + deleteOriginal = false, + compressIfPossible = false, + ).getOrThrow() + assertThat(result.file.path).endsWith("video.mp4") + val info = result as MediaUploadInfo.Video + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.videoInfo).isEqualTo( + VideoInfo( + duration = Duration.ZERO, // Not available with Robolectric? + height = 0, // Not available with Robolectric? + width = 0, // Not available with Robolectric? + mimetype = MimeTypes.Mp4, + size = 1_673_712, + thumbnailInfo = ThumbnailInfo(height = null, width = null, mimetype = MimeTypes.Jpeg, size = 0), // Not available with Robolectric? + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing audio`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "sample3s.mp3") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Mp3, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("sample3s.mp3") + val info = result as MediaUploadInfo.Audio + assertThat(info.audioInfo).isEqualTo( + AudioInfo( + duration = Duration.ZERO, // Not available with Robolectric? + size = 52_079, + mimetype = MimeTypes.Mp3, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test file which does not exist`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = File(context.cacheDir, "not found.txt") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.PlainText, + deleteOriginal = false, + compressIfPossible = true, + ) + assertThat(result.isFailure).isTrue() + val failure = result.exceptionOrNull() + assertThat(failure).isInstanceOf(MediaPreProcessor.Failure::class.java) + assertThat(failure?.cause).isInstanceOf(FileNotFoundException::class.java) + } + + private fun TestScope.createAndroidMediaPreProcessor( + context: Context, + sdkIntVersion: Int = Build.VERSION_CODES.P + ) = AndroidMediaPreProcessor( + context = context, + thumbnailFactory = ThumbnailFactory(context, FakeBuildVersionSdkIntProvider(sdkIntVersion)), + imageCompressor = ImageCompressor(context, testCoroutineDispatchers()), + videoCompressor = VideoCompressor(context), + coroutineDispatchers = testCoroutineDispatchers(), + ) + + @Throws(IOException::class) + private fun getFileFromAssets(context: Context, fileName: String): File = File(context.cacheDir, fileName) + .also { + if (!it.exists()) { + it.outputStream().use { cache -> + context.assets.open(fileName).use { inputStream -> + inputStream.copyTo(cache) + } + } + } + } +} diff --git a/libraries/mediaupload/test/build.gradle.kts b/libraries/mediaupload/test/build.gradle.kts index 956afffbe0..a0d3ed35cc 100644 --- a/libraries/mediaupload/test/build.gradle.kts +++ b/libraries/mediaupload/test/build.gradle.kts @@ -24,5 +24,6 @@ android { dependencies { api(projects.libraries.mediaupload.api) + implementation(projects.libraries.core) implementation(projects.tests.testutils) } 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 8e7e71e8fb..0c612c8d1c 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,6 +17,7 @@ package io.element.android.libraries.mediaupload.test import android.net.Uri +import io.element.android.libraries.core.mimetype.MimeTypes 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 @@ -24,7 +25,6 @@ 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 { @@ -35,7 +35,7 @@ class FakeMediaPreProcessor : MediaPreProcessor { MediaUploadInfo.AnyFile( File("test"), FileInfo( - mimetype = "*/*", + mimetype = MimeTypes.Any, size = 999L, thumbnailInfo = null, thumbnailSource = null, @@ -63,9 +63,9 @@ class FakeMediaPreProcessor : MediaPreProcessor { MediaUploadInfo.Audio( file = File("audio.ogg"), audioInfo = AudioInfo( - duration = 1000.seconds.toJavaDuration(), + duration = 1000.seconds, size = 1000, - mimetype = "audio/ogg", + mimetype = MimeTypes.Ogg, ), ) ) diff --git a/libraries/mediaviewer/api/build.gradle.kts b/libraries/mediaviewer/api/build.gradle.kts new file mode 100644 index 0000000000..7076a144fe --- /dev/null +++ b/libraries/mediaviewer/api/build.gradle.kts @@ -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. + */ +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.libraries.mediaviewer.api" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(projects.anvilannotations) + + implementation(libs.coil.compose) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.ui) + implementation(libs.coroutines.core) + implementation(libs.dagger) + implementation(libs.telephoto.zoomableimage) + implementation(libs.vanniktech.blurhash) + + implementation(projects.libraries.androidutils) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.uiStrings) + + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.mediaviewer.test) + testImplementation(projects.tests.testutils) + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.turbine) + testImplementation(libs.coroutines.core) + testImplementation(libs.coroutines.test) + + ksp(libs.showkase.processor) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/helper/fileExtensionAndSize.kt similarity index 93% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/helper/fileExtensionAndSize.kt index 251d7a0f06..a2a4caec44 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/helper/fileExtensionAndSize.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.helper +package io.element.android.libraries.mediaviewer.api.helper fun formatFileExtensionAndSize(extension: String, size: String?): String { return buildString { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMedia.kt similarity index 93% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMedia.kt index 549842428a..067979dc93 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMedia.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.mediaviewer.api.local import android.net.Uri import android.os.Parcelable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaActions.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaActions.kt index f35af36057..158c748a94 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaActions.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.mediaviewer.api.local import androidx.compose.runtime.Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt index 36852a5a80..64dfbd03d8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.mediaviewer.api.local import android.net.Uri import io.element.android.libraries.matrix.api.media.MediaFile diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt index 62ddf353fa..1e81f75e9f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.mediaviewer.api.local import android.annotation.SuppressLint import android.net.Uri @@ -56,10 +56,9 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize -import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper -import io.element.android.features.messages.impl.media.local.pdf.PdfViewer -import io.element.android.features.messages.impl.media.local.pdf.rememberPdfViewerState +import io.element.android.libraries.mediaviewer.api.local.exoplayer.ExoPlayerWrapper +import io.element.android.libraries.mediaviewer.api.local.pdf.PdfViewer +import io.element.android.libraries.mediaviewer.api.local.pdf.rememberPdfViewerState import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio @@ -70,6 +69,7 @@ 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.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.ui.strings.CommonStrings import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.ZoomableState diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaViewState.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt similarity index 94% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaViewState.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt index d5af7a78e1..07f891c90c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaViewState.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaViewState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.mediaviewer.api.local import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt index af0f142bd8..726c9dcf0b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.mediaviewer.api.local import android.os.Parcelable import io.element.android.libraries.core.mimetype.MimeTypes diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/ExoPlayerWrapper.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/exoplayer/ExoPlayerWrapper.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/ExoPlayerWrapper.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/exoplayer/ExoPlayerWrapper.kt index a69db1ef2c..581a018c43 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/ExoPlayerWrapper.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/exoplayer/ExoPlayerWrapper.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local.exoplayer +package io.element.android.libraries.mediaviewer.api.local.exoplayer import android.content.Context import androidx.media3.common.Player diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/ParcelFileDescriptorFactory.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/ParcelFileDescriptorFactory.kt similarity index 94% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/ParcelFileDescriptorFactory.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/ParcelFileDescriptorFactory.kt index 22233b313f..523e8b593c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/ParcelFileDescriptorFactory.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/ParcelFileDescriptorFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local.pdf +package io.element.android.libraries.mediaviewer.api.local.pdf import android.content.Context import android.net.Uri diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfPage.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfPage.kt similarity index 98% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfPage.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfPage.kt index 0b8caed968..1da6d1a21c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfPage.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfPage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local.pdf +package io.element.android.libraries.mediaviewer.api.local.pdf import android.graphics.Bitmap import android.graphics.Canvas diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfRendererManager.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfRendererManager.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt index 8f6c507eb5..56fe175647 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfRendererManager.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfRendererManager.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local.pdf +package io.element.android.libraries.mediaviewer.api.local.pdf import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfViewer.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt similarity index 98% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfViewer.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt index 9940d22542..1bad0e75f5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfViewer.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewer.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local.pdf +package io.element.android.libraries.mediaviewer.api.local.pdf import androidx.compose.foundation.Image import androidx.compose.foundation.background diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfViewerState.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfViewerState.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt index f64374d478..8df8bfdecf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/pdf/PdfViewerState.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/pdf/PdfViewerState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local.pdf +package io.element.android.libraries.mediaviewer.api.local.pdf import android.content.Context import androidx.compose.foundation.lazy.LazyListState diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileExtensionExtractor.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileExtensionExtractor.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt index bdb4e0a23d..81a727909a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/FileExtensionExtractor.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.timeline.util +package io.element.android.libraries.mediaviewer.api.util import android.webkit.MimeTypeMap import com.squareup.anvil.annotations.ContributesBinding diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerEvents.kt similarity index 93% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerEvents.kt index a3d9632f18..b9f8b0aa4d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer +package io.element.android.libraries.mediaviewer.api.viewer sealed interface MediaViewerEvents { data object SaveOnDisk: MediaViewerEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerNode.kt similarity index 89% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerNode.kt index 30440f5ceb..3917d2eba4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer +package io.element.android.libraries.mediaviewer.api.viewer import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -25,14 +25,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ForcedDarkElementTheme -import io.element.android.features.messages.impl.media.local.MediaInfo 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.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.mediaviewer.api.local.MediaInfo @ContributesNode(RoomScope::class) -class MediaViewerNode @AssistedInject constructor( +open class MediaViewerNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, presenterFactory: MediaViewerPresenter.Factory, @@ -42,6 +42,8 @@ class MediaViewerNode @AssistedInject constructor( val mediaInfo: MediaInfo, val mediaSource: MediaSource, val thumbnailSource: MediaSource?, + val canDownload: Boolean, + val canShare: Boolean, ) : NodeInputs private val inputs: Inputs = inputs() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerPresenter.kt similarity index 94% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerPresenter.kt index 91fff1fa72..90d81a4d0c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer +package io.element.android.libraries.mediaviewer.api.viewer import android.content.ActivityNotFoundException import androidx.compose.runtime.Composable @@ -29,9 +29,6 @@ import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.LocalMediaActions -import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher @@ -39,6 +36,9 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions +import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -91,6 +91,8 @@ class MediaViewerPresenter @AssistedInject constructor( thumbnailSource = inputs.thumbnailSource, downloadedMedia = localMedia.value, snackbarMessage = snackbarMessage, + canDownload = inputs.canDownload, + canShare = inputs.canShare, eventSink = ::handleEvents ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerState.kt similarity index 80% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerState.kt index 86abaf648e..4b68f4afcc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerState.kt @@ -14,18 +14,20 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer +package io.element.android.libraries.mediaviewer.api.viewer -import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.MediaInfo data class MediaViewerState( val mediaInfo: MediaInfo, val thumbnailSource: MediaSource?, val downloadedMedia: Async, val snackbarMessage: SnackbarMessage?, + val canDownload: Boolean, + val canShare: Boolean, val eventSink: (MediaViewerEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt similarity index 72% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt index 1042261be8..14f792c2c6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerStateProvider.kt @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer +package io.element.android.libraries.mediaviewer.api.viewer import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.MediaInfo -import io.element.android.features.messages.impl.media.local.aFileInfo -import io.element.android.features.messages.impl.media.local.aPdfInfo -import io.element.android.features.messages.impl.media.local.aVideoInfo -import io.element.android.features.messages.impl.media.local.anAudioInfo -import io.element.android.features.messages.impl.media.local.anImageInfo import io.element.android.libraries.architecture.Async +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.local.aFileInfo +import io.element.android.libraries.mediaviewer.api.local.aPdfInfo +import io.element.android.libraries.mediaviewer.api.local.aVideoInfo +import io.element.android.libraries.mediaviewer.api.local.anAudioInfo +import io.element.android.libraries.mediaviewer.api.local.anImageInfo open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence @@ -71,15 +71,27 @@ open class MediaViewerStateProvider : PreviewParameterProvider ), anAudioInfo(), ), + aMediaViewerState( + Async.Success( + LocalMedia(Uri.EMPTY, anImageInfo()) + ), + anImageInfo(), + canDownload = false, + canShare = false, + ), ) } fun aMediaViewerState( downloadedMedia: Async = Async.Uninitialized, mediaInfo: MediaInfo = anImageInfo(), + canDownload: Boolean = true, + canShare: Boolean = true, ) = MediaViewerState( mediaInfo = mediaInfo, thumbnailSource = null, downloadedMedia = downloadedMedia, - snackbarMessage = null + snackbarMessage = null, + canDownload = canDownload, + canShare = canShare, ) {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt similarity index 84% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt rename to libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt index 66cb0e6ba3..d7f743f7ad 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package io.element.android.features.messages.impl.media.viewer +package io.element.android.libraries.mediaviewer.api.viewer import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -47,11 +47,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import io.element.android.features.messages.impl.R -import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.LocalMediaView -import io.element.android.features.messages.impl.media.local.MediaInfo -import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.button.BackButton @@ -66,6 +61,11 @@ 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.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.mediaviewer.api.R +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaView +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.local.rememberLocalMediaViewState import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.delay @@ -96,6 +96,8 @@ fun MediaViewerView( actionsEnabled = state.downloadedMedia is Async.Success, mimeType = state.mediaInfo.mimeType, onBackPressed = onBackPressed, + canDownload = state.canDownload, + canShare = state.canShare, eventSink = state.eventSink ) }, @@ -162,9 +164,12 @@ private fun rememberShowProgress(downloadedMedia: Async): Boolean { return showProgress } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun MediaViewerTopBar( actionsEnabled: Boolean, + canDownload: Boolean, + canShare: Boolean, mimeType: String, onBackPressed: () -> Unit, eventSink: (MediaViewerEvents) -> Unit, @@ -190,27 +195,31 @@ private fun MediaViewerTopBar( ) } } - IconButton( - enabled = actionsEnabled, - onClick = { - eventSink(MediaViewerEvents.SaveOnDisk) - }, - ) { - Icon( - resourceId = CompoundDrawables.ic_download, - contentDescription = stringResource(id = CommonStrings.action_save), - ) + if (canDownload) { + IconButton( + enabled = actionsEnabled, + onClick = { + eventSink(MediaViewerEvents.SaveOnDisk) + }, + ) { + Icon( + resourceId = CompoundDrawables.ic_download, + contentDescription = stringResource(id = CommonStrings.action_save), + ) + } } - IconButton( - enabled = actionsEnabled, - onClick = { - eventSink(MediaViewerEvents.Share) - }, - ) { - Icon( - resourceId = CompoundDrawables.ic_share_android, - contentDescription = stringResource(id = CommonStrings.action_share) - ) + if (canShare) { + IconButton( + enabled = actionsEnabled, + onClick = { + eventSink(MediaViewerEvents.Share) + }, + ) { + Icon( + resourceId = CompoundDrawables.ic_share_android, + contentDescription = stringResource(id = CommonStrings.action_share) + ) + } } } ) diff --git a/features/messages/impl/src/main/res/drawable/ic_apk_install.xml b/libraries/mediaviewer/api/src/main/res/drawable/ic_apk_install.xml similarity index 100% rename from features/messages/impl/src/main/res/drawable/ic_apk_install.xml rename to libraries/mediaviewer/api/src/main/res/drawable/ic_apk_install.xml diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt similarity index 90% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenterTest.kt rename to libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt index dfd648bd22..ce2b4651a5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/MediaViewerPresenterTest.kt @@ -16,20 +16,23 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.features.messages.impl.media.viewer +package io.element.android.libraries.mediaviewer import android.net.Uri 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.media.FakeLocalMediaActions -import io.element.android.features.messages.impl.media.FakeLocalMediaFactory -import io.element.android.features.messages.impl.media.local.aFileInfo import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.media.aMediaSource +import io.element.android.libraries.mediaviewer.api.local.aFileInfo +import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions +import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory +import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerEvents +import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode +import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter import io.element.android.tests.testutils.WarmUpRule import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -148,12 +151,16 @@ class MediaViewerPresenterTest { mediaLoader: FakeMediaLoader, localMediaActions: FakeLocalMediaActions, snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), + canShare: Boolean = true, + canDownload: Boolean = true, ): MediaViewerPresenter { return MediaViewerPresenter( inputs = MediaViewerNode.Inputs( mediaInfo = TESTED_MEDIA_INFO, mediaSource = aMediaSource(), - thumbnailSource = null + thumbnailSource = null, + canShare = canShare, + canDownload = canDownload, ), localMediaFactory = localMediaFactory, mediaLoader = mediaLoader, diff --git a/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractorTest.kt b/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractorTest.kt new file mode 100644 index 0000000000..a2795d2884 --- /dev/null +++ b/libraries/mediaviewer/api/src/test/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractorTest.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.mediaviewer.api.util + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class FileExtensionExtractorTest { + @Test + fun `test FileExtensionExtractor with validation OK`() { + val sut = FileExtensionExtractorWithValidation() + // The result should be txt, but with Robolectric, + // MimeTypeMap.getSingleton().hasExtension() always returns false + assertThat(sut.extractFromName("test.txt")).isEqualTo("bin") + } + + @Test + fun `test FileExtensionExtractor with validation ERROR`() { + val sut = FileExtensionExtractorWithValidation() + assertThat(sut.extractFromName("test.bla")).isEqualTo("bin") + } + + @Test + fun `test FileExtensionExtractor no validation`() { + val sut = FileExtensionExtractorWithoutValidation() + assertThat(sut.extractFromName("test.png")).isEqualTo("png") + assertThat(sut.extractFromName("test.bla")).isEqualTo("bla") + } +} diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts new file mode 100644 index 0000000000..0ccfd0b65e --- /dev/null +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -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. + */ +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.libraries.mediaviewer.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + anvil(projects.anvilcodegen) + implementation(projects.anvilannotations) + + implementation(libs.coroutines.core) + implementation(libs.dagger) + + api(projects.libraries.mediaviewer.api) + implementation(projects.libraries.androidutils) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + implementation(projects.libraries.matrix.api) + + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.mediaviewer.test) + testImplementation(projects.tests.testutils) + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(libs.test.mockk) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) + 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/media/local/AndroidLocalMediaActions.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt index ef93574134..35ebc0e022 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.mediaviewer.impl.local import android.app.Activity import android.content.ContentResolver @@ -43,6 +43,8 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt similarity index 88% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt index 4ed08edf6f..1c026ce53b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt @@ -14,13 +14,12 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.mediaviewer.impl.local import android.content.Context import android.net.Uri import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileSize import io.element.android.libraries.androidutils.file.getMimeType @@ -30,6 +29,10 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.toFile +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor import javax.inject.Inject @ContributesBinding(AppScope::class) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActionsTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActionsTest.kt new file mode 100644 index 0000000000..0e981d73af --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActionsTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.mediaviewer.impl.local + +import android.net.Uri +import androidx.activity.compose.LocalActivityResultRegistryOwner +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +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.matrix.test.core.aBuildMeta +import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class AndroidLocalMediaActionsTest { + + @Test + fun `present - AndroidLocalMediaAction configure`() = runTest { + val sut = createAndroidLocalMediaActions() + moleculeFlow(RecompositionMode.Immediate) { + CompositionLocalProvider( + LocalContext provides RuntimeEnvironment.getApplication(), + LocalActivityResultRegistryOwner provides NoOpActivityResultRegistryOwner() + ) { + sut.Configure() + } + }.test { + awaitItem() + } + } + + @Test + fun `test AndroidLocalMediaAction share`() = runTest { + val sut = createAndroidLocalMediaActions() + val result = sut.share(aLocalMedia(Uri.parse("file://afile"))) + assertThat(result.exceptionOrNull()).isNotNull() + } + + @Test + fun `test AndroidLocalMediaAction open`() = runTest { + val sut = createAndroidLocalMediaActions() + val result = sut.open(aLocalMedia(Uri.parse("file://afile"))) + assertThat(result.exceptionOrNull()).isNotNull() + } + + @Test + fun `test AndroidLocalMediaAction save on disk`() = runTest { + val sut = createAndroidLocalMediaActions() + val result = sut.saveOnDisk(aLocalMedia(Uri.parse("file://afile"))) + assertThat(result.exceptionOrNull()).isNotNull() + } + + private fun TestScope.createAndroidLocalMediaActions() = AndroidLocalMediaActions( + RuntimeEnvironment.getApplication(), + testCoroutineDispatchers(), + aBuildMeta() + ) +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt new file mode 100644 index 0000000000..c6209ae7c3 --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.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.mediaviewer.impl.local + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.matrix.test.media.FakeMediaFile +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.local.anImageInfo +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class AndroidLocalMediaFactoryTest { + + @Test + fun `test AndroidLocalMediaFactory`() { + val sut = createAndroidLocalMediaFactory() + val result = sut.createFromMediaFile(aMediaFile(), anImageInfo()) + assertThat(result.uri.toString()).endsWith("aPath") + assertThat(result.info).isEqualTo( + MediaInfo( + name = "an image file.jpg", + mimeType = MimeTypes.Jpeg, + formattedFileSize = "4MB", + fileExtension = "jpg", + ) + ) + } + + private fun aMediaFile(): MediaFile { + return FakeMediaFile("aPath") + } + + private fun createAndroidLocalMediaFactory(): AndroidLocalMediaFactory { + return AndroidLocalMediaFactory( + RuntimeEnvironment.getApplication(), + FakeFileSizeFormatter(), + FileExtensionExtractorWithoutValidation() + ) + } +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/NoOpActivityResultRegistryOwner.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/NoOpActivityResultRegistryOwner.kt new file mode 100644 index 0000000000..9d774bb163 --- /dev/null +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/NoOpActivityResultRegistryOwner.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.mediaviewer.impl.local + +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.ActivityResultRegistryOwner +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.app.ActivityOptionsCompat + +class NoOpActivityResultRegistryOwner : ActivityResultRegistryOwner { + override val activityResultRegistry: ActivityResultRegistry + get() = NoOpActivityResultRegistry() +} + +class NoOpActivityResultRegistry : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat?, + ) = Unit +} diff --git a/libraries/mediaviewer/test/build.gradle.kts b/libraries/mediaviewer/test/build.gradle.kts new file mode 100644 index 0000000000..6bbedd7f1d --- /dev/null +++ b/libraries/mediaviewer/test/build.gradle.kts @@ -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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.mediaviewer.test" +} + +dependencies { + api(projects.libraries.mediaviewer.impl) + implementation(projects.libraries.core) + implementation(projects.tests.testutils) + implementation(projects.libraries.matrix.api) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/FakeLocalMediaActions.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt similarity index 88% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/FakeLocalMediaActions.kt rename to libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt index 351bb6aeca..8c303de541 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/FakeLocalMediaActions.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaActions.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media +package io.element.android.libraries.mediaviewer.test import androidx.compose.runtime.Composable -import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.LocalMediaActions +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaActions import io.element.android.tests.testutils.simulateLongTask class FakeLocalMediaActions : LocalMediaActions { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/FakeLocalMediaFactory.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt similarity index 76% rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/FakeLocalMediaFactory.kt rename to libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt index 724c6e7f55..84f1d87613 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/media/FakeLocalMediaFactory.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt @@ -14,17 +14,17 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media +package io.element.android.libraries.mediaviewer.test import android.net.Uri -import io.element.android.features.messages.impl.fixtures.aLocalMedia -import io.element.android.features.messages.impl.media.local.LocalMedia -import io.element.android.features.messages.impl.media.local.LocalMediaFactory -import io.element.android.features.messages.impl.media.local.MediaInfo -import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractor -import io.element.android.features.messages.impl.timeline.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor +import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation class FakeLocalMediaFactory( private val localMediaUri: Uri, diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/media.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/media.kt new file mode 100644 index 0000000000..e19113c163 --- /dev/null +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/viewer/media.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.mediaviewer.test.viewer + +import android.net.Uri +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.MediaInfo +import io.element.android.libraries.mediaviewer.api.local.anImageInfo + +fun aLocalMedia( + uri: Uri, + mediaInfo: MediaInfo = anImageInfo(), +) = LocalMedia( + uri = uri, + info = mediaInfo +) + diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 6c765fc44f..f72a0886a0 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -65,8 +65,10 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.coil.test) testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.toolbox.impl) testImplementation(projects.services.toolbox.test) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index f2e3240203..09f622e978 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 @@ -36,6 +36,7 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.currentSessionId import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -61,6 +62,8 @@ class DefaultNotificationDrawerManager @Inject constructor( private val buildMeta: BuildMeta, private val matrixClientProvider: MatrixClientProvider, ) : NotificationDrawerManager { + private var appNavigationStateObserver: Job? = null + /** * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. */ @@ -72,12 +75,17 @@ class DefaultNotificationDrawerManager @Inject constructor( init { // Observe application state - coroutineScope.launch { + appNavigationStateObserver = coroutineScope.launch { appNavigationStateService.appNavigationState .collect { onAppNavigationStateChange(it.navigationState) } } } + // For test only + fun destroy() { + appNavigationStateObserver?.cancel() + } + private var currentAppNavigationState: NavigationState? = null private fun onAppNavigationStateChange(navigationState: NavigationState) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistence.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistence.kt new file mode 100644 index 0000000000..3646837937 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistence.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.file.EncryptedFileFactory +import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import timber.log.Timber +import java.io.File +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import javax.inject.Inject + +private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache" +private const val FILE_NAME = "notifications.bin" + +private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag) + +@ContributesBinding(AppScope::class) +class DefaultNotificationEventPersistence @Inject constructor( + @ApplicationContext private val context: Context, +) : NotificationEventPersistence { + private val file by lazy { + deleteLegacyFileIfAny() + context.getDatabasePath(FILE_NAME) + } + + private val encryptedFile by lazy { + EncryptedFileFactory(context).create(file) + } + + override fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue { + val rawEvents: ArrayList? = file + .takeIf { it.exists() } + ?.let { + try { + encryptedFile.openFileInput().use { fis -> + ObjectInputStream(fis).use { ois -> + @Suppress("UNCHECKED_CAST") + ois.readObject() as? ArrayList + } + }.also { + Timber.tag(loggerTag.value).d("Deserializing ${it?.size} NotifiableEvent(s)") + } + } catch (e: Throwable) { + Timber.tag(loggerTag.value).e(e, "## Failed to load cached notification info") + null + } + } + return factory(rawEvents.orEmpty()) + } + + override fun persistEvents(queuedEvents: NotificationEventQueue) { + Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)") + // Always delete file before writing, or encryptedFile.openFileOutput() will throw + file.safeDelete() + if (queuedEvents.isEmpty()) return + try { + encryptedFile.openFileOutput().use { fos -> + ObjectOutputStream(fos).use { oos -> + oos.writeObject(queuedEvents.rawEvents()) + } + } + } catch (e: Throwable) { + Timber.tag(loggerTag.value).e(e, "## Failed to save cached notification info") + } + } + + private fun deleteLegacyFileIfAny() { + tryOrNull { + File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete() + } + } +} 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 9093b36615..26c8a7d55b 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 @@ -82,6 +82,11 @@ class NotifiableEventResolver @Inject constructor( return when (val content = this.content) { is NotificationContent.MessageLike.RoomMessage -> { val messageBody = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value) + val notificationBody = if (hasMention) { + stringProvider.getString(R.string.notification_mentioned_you_body, messageBody) + } else { + messageBody + } buildNotifiableMessageEvent( sessionId = userId, senderId = content.senderId, @@ -90,7 +95,7 @@ class NotifiableEventResolver @Inject constructor( noisy = isNoisy, timestamp = this.timestamp, senderName = senderDisplayName, - body = messageBody, + body = notificationBody, imageUriString = this.contentUrl, roomName = roomDisplayName, roomIsDirect = isDirect, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt index 1ad38bf787..e78ce37639 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt @@ -27,11 +27,13 @@ import coil.transform.CircleCropTransformation import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import timber.log.Timber import javax.inject.Inject class NotificationBitmapLoader @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val sdkIntProvider: BuildVersionSdkIntProvider, ) { /** @@ -65,7 +67,7 @@ class NotificationBitmapLoader @Inject constructor( * @param path mxc url */ suspend fun getUserIcon(path: String?): IconCompat? { - if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) { return null } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt index d1aee8c0b2..e593f49824 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 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. @@ -16,76 +16,9 @@ package io.element.android.libraries.push.impl.notifications -import android.content.Context -import io.element.android.libraries.androidutils.file.EncryptedFileFactory -import io.element.android.libraries.androidutils.file.safeDelete -import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent -import timber.log.Timber -import java.io.File -import java.io.ObjectInputStream -import java.io.ObjectOutputStream -import javax.inject.Inject -private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache" -private const val FILE_NAME = "notifications.bin" - -private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag) - -class NotificationEventPersistence @Inject constructor( - @ApplicationContext private val context: Context, -) { - private val file by lazy { - deleteLegacyFileIfAny() - context.getDatabasePath(FILE_NAME) - } - - private val encryptedFile by lazy { - EncryptedFileFactory(context).create(file) - } - - fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue { - val rawEvents: ArrayList? = file - .takeIf { it.exists() } - ?.let { - try { - encryptedFile.openFileInput().use { fis -> - ObjectInputStream(fis).use { ois -> - @Suppress("UNCHECKED_CAST") - ois.readObject() as? ArrayList - } - }.also { - Timber.tag(loggerTag.value).d("Deserializing ${it?.size} NotifiableEvent(s)") - } - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## Failed to load cached notification info") - null - } - } - return factory(rawEvents.orEmpty()) - } - - fun persistEvents(queuedEvents: NotificationEventQueue) { - Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)") - // Always delete file before writing, or encryptedFile.openFileOutput() will throw - file.safeDelete() - if (queuedEvents.isEmpty()) return - try { - encryptedFile.openFileOutput().use { fos -> - ObjectOutputStream(fos).use { oos -> - oos.writeObject(queuedEvents.rawEvents()) - } - } - } catch (e: Throwable) { - Timber.tag(loggerTag.value).e(e, "## Failed to save cached notification info") - } - } - - private fun deleteLegacyFileIfAny() { - tryOrNull { - File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete() - } - } +interface NotificationEventPersistence { + fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue + fun persistEvents(queuedEvents: NotificationEventQueue) } 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 8eea6a9d5a..bb76de47d2 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 @@ -28,7 +28,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import timber.log.Timber -data class NotificationEventQueue constructor( +data class NotificationEventQueue( private val queue: MutableList, /** * An in memory FIFO cache of the seen events. diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt index 859bff17cf..15c236eac9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser -import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -28,9 +28,8 @@ import javax.inject.Inject private typealias ProcessedMessageEvents = List> -// TODO Find a better name, it clashes with io.element.android.libraries.push.impl.notifications.factories.NotificationFactory class NotificationFactory @Inject constructor( - private val notificationFactory: NotificationFactory, + private val notificationCreator: NotificationCreator, private val roomGroupMessageCreator: RoomGroupMessageCreator, private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { @@ -65,7 +64,7 @@ class NotificationFactory @Inject constructor( when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationFactory.createRoomInvitationNotification(event), + notificationCreator.createRoomInvitationNotification(event), OneShotNotification.Append.Meta( key = event.roomId.value, summaryLine = event.description, @@ -83,7 +82,7 @@ class NotificationFactory @Inject constructor( when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationFactory.createSimpleEventNotification(event), + notificationCreator.createSimpleEventNotification(event), OneShotNotification.Append.Meta( key = event.eventId.value, summaryLine = event.description, @@ -100,7 +99,7 @@ class NotificationFactory @Inject constructor( when (processed) { ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value) ProcessedEvent.Type.KEEP -> OneShotNotification.Append( - notificationFactory.createFallbackNotification(event), + notificationCreator.createFallbackNotification(event), OneShotNotification.Append.Meta( key = event.eventId.value, summaryLine = event.description.orEmpty(), diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 29d828d34c..cde4f489a2 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -27,16 +27,15 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug -import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.services.toolbox.api.strings.StringProvider -import timber.log.Timber import javax.inject.Inject class RoomGroupMessageCreator @Inject constructor( private val bitmapLoader: NotificationBitmapLoader, private val stringProvider: StringProvider, - private val notificationFactory: NotificationFactory + private val notificationCreator: NotificationCreator ) { suspend fun createRoomMessage( @@ -78,7 +77,7 @@ class RoomGroupMessageCreator @Inject constructor( shouldBing = events.any { it.noisy } ) return RoomNotification.Message( - notificationFactory.createMessagesListNotification( + notificationCreator.createMessagesListNotification( style, RoomEventGroupInfo( sessionId = currentUser.userId, @@ -133,22 +132,16 @@ class RoomGroupMessageCreator @Inject constructor( } private fun createRoomMessagesGroupSummaryLine(events: List, roomName: String, roomIsDirect: Boolean): CharSequence { - return try { - when (events.size) { - 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect) - else -> { - stringProvider.getQuantityString( - R.plurals.notification_compat_summary_line_for_room, - events.size, - roomName, - events.size - ) - } + return when (events.size) { + 1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect) + else -> { + stringProvider.getQuantityString( + R.plurals.notification_compat_summary_line_for_room, + events.size, + roomName, + events.size + ) } - } catch (e: Throwable) { - // String not found or bad format - Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string") - roomName } } @@ -166,7 +159,7 @@ class RoomGroupMessageCreator @Inject constructor( inSpans(StyleSpan(Typeface.BOLD)) { append(roomName) append(": ") - event.senderName + append(event.senderName) append(" ") } append(event.description) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index f999456107..f46df7d3a5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -21,7 +21,7 @@ import androidx.core.app.NotificationCompat import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug -import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject @@ -41,7 +41,7 @@ import javax.inject.Inject */ class SummaryGroupMessageCreator @Inject constructor( private val stringProvider: StringProvider, - private val notificationFactory: NotificationFactory, + private val notificationCreator: NotificationCreator, ) { fun createSummaryNotification( @@ -77,7 +77,7 @@ class SummaryGroupMessageCreator @Inject constructor( // Use account name now, for multi-session .setSummaryText(currentUser.userId.value.annotateForDebug(44)) return if (useCompleteNotificationFormat) { - notificationFactory.createSummaryListNotification( + notificationCreator.createSummaryListNotification( currentUser, summaryInboxStyle, sumTitle, @@ -170,7 +170,7 @@ class SummaryGroupMessageCreator @Inject constructor( messageStr } } - return notificationFactory.createSummaryListNotification( + return notificationCreator.createSummaryListNotification( currentUser = currentUser, style = null, compatSummary = privacyTitle, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt index ab115262de..74020f9323 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.push.impl.notifications.channels -import android.app.Activity import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context @@ -24,7 +23,6 @@ import android.os.Build import androidx.annotation.ChecksSdkIntAtLeast import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import io.element.android.libraries.androidutils.system.startNotificationChannelSettingsIntent import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn @@ -163,17 +161,5 @@ class NotificationChannels @Inject constructor( @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - - fun openSystemSettingsForSilentCategory(activity: Activity) { - activity.startNotificationChannelSettingsIntent(SILENT_NOTIFICATION_CHANNEL_ID) - } - - fun openSystemSettingsForNoisyCategory(activity: Activity) { - activity.startNotificationChannelSettingsIntent(NOISY_NOTIFICATION_CHANNEL_ID) - } - - fun openSystemSettingsForCallCategory(activity: Activity) { - activity.startNotificationChannelSettingsIntent(CALL_NOTIFICATION_CHANNEL_ID) - } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt similarity index 99% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index 105b5789e4..2beddc53f9 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -41,7 +41,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject -class NotificationFactory @Inject constructor( +class NotificationCreator @Inject constructor( @ApplicationContext private val context: Context, private val notificationChannels: NotificationChannels, private val stringProvider: StringProvider, diff --git a/libraries/push/impl/src/main/res/values-cs/translations.xml b/libraries/push/impl/src/main/res/values-cs/translations.xml index b9488e008b..2b76d7b93e 100644 --- a/libraries/push/impl/src/main/res/values-cs/translations.xml +++ b/libraries/push/impl/src/main/res/values-cs/translations.xml @@ -8,10 +8,7 @@ "Vstoupit" "Odmítnout" "Vás pozval(a) do chatu" - "%1$s vás zmínil(a). -%2$s" - "Byli jste zmíněni. -%1$s" + "Zmínili vás: %1$s" "Nové zprávy" "Reagoval(a) s %1$s" "Označit jako přečtené" diff --git a/libraries/push/impl/src/main/res/values-ru/translations.xml b/libraries/push/impl/src/main/res/values-ru/translations.xml index aec7180374..f183396dbc 100644 --- a/libraries/push/impl/src/main/res/values-ru/translations.xml +++ b/libraries/push/impl/src/main/res/values-ru/translations.xml @@ -8,10 +8,7 @@ "Присоединиться" "Отклонить" "Пригласил вас в чат" - "%1$s упомянул вас. -%2$s" - "Вас уже упомянули. -%1$s" + "Упомянул вас: %1$s" "Новые сообщения" "Отреагировал на %1$s" "Отметить как прочитанное" diff --git a/libraries/push/impl/src/main/res/values-sk/translations.xml b/libraries/push/impl/src/main/res/values-sk/translations.xml index 0bc3338204..bd79b42fc3 100644 --- a/libraries/push/impl/src/main/res/values-sk/translations.xml +++ b/libraries/push/impl/src/main/res/values-sk/translations.xml @@ -8,10 +8,7 @@ "Pripojiť sa" "Zamietnuť" "Vás pozval/a na konverzáciu" - "%1$s vás spomenul/-a. -%2$s" - "Boli ste spomenutí. -%1$s" + "Spomenul/a vás: %1$s" "Nové správy" "Reagoval/a s %1$s" "Označiť ako prečítané" diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 5cadfb90ac..7d50cd2864 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -8,10 +8,7 @@ "Join" "Reject" "Invited you to chat" - "%1$s mentioned you. -%2$s" - "You have been mentioned. -%1$s" + "Mentioned you: %1$s" "New Messages" "Reacted with %1$s" "Mark as read" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt new file mode 100644 index 0000000000..6b4ea24fd4 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.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.push.impl.notifications + +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_SPACE_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory +import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.NavigationState +import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import io.element.android.services.appnavstate.test.aNavigationState +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class DefaultNotificationDrawerManagerTest { + @Test + fun `clearAllEvents should have no effect when queue is empty`() = runTest { + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() + defaultNotificationDrawerManager.clearAllEvents(A_SESSION_ID) + defaultNotificationDrawerManager.destroy() + } + + @Test + fun `cover all APIs`() = runTest { + // For now just call all the API. Later, add more valuable tests. + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() + defaultNotificationDrawerManager.notificationStyleChanged() + defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true) + defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false) + defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true) + defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = false) + defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true) + defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false) + defaultNotificationDrawerManager.clearMembershipNotificationForSession(A_SESSION_ID) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false) + defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) + // Add the same Event again (will be ignored) + defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) + defaultNotificationDrawerManager.destroy() + } + + @Test + fun `react to applicationStateChange`() = runTest { + // For now just call all the API. Later, add more valuable tests. + val appNavigationStateFlow: MutableStateFlow = MutableStateFlow( + AppNavigationState( + navigationState = NavigationState.Root, + isInForeground = true, + ) + ) + val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow) + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( + appNavigationStateService = appNavigationStateService + ) + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true)) + runCurrent() + // Like a user sign out + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) + runCurrent() + defaultNotificationDrawerManager.destroy() + } + + private fun TestScope.createDefaultNotificationDrawerManager( + appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(), + initialData: List = emptyList() + ): DefaultNotificationDrawerManager { + val context = RuntimeEnvironment.getApplication() + return DefaultNotificationDrawerManager( + notifiableEventProcessor = NotifiableEventProcessor( + outdatedDetector = OutdatedEventDetector(), + appNavigationStateService = appNavigationStateService + ), + notificationRenderer = NotificationRenderer( + NotificationIdProvider(), + NotificationDisplayer(context), + NotificationFactory( + FakeAndroidNotificationFactory().instance, + FakeRoomGroupMessageCreator().instance, + FakeSummaryGroupMessageCreator().instance, + ) + ), + notificationEventPersistence = InMemoryNotificationEventPersistence(initialData = initialData), + filteredEventDetector = FilteredEventDetector(), + appNavigationStateService = appNavigationStateService, + coroutineScope = this, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + buildMeta = aBuildMeta(), + matrixClientProvider = FakeMatrixClientProvider(), + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistenceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistenceTest.kt new file mode 100644 index 0000000000..ce984c712f --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationEventPersistenceTest.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.libraries.push.impl.notifications + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.cache.CircularCache +import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class DefaultNotificationEventPersistenceTest { + @Test + fun `loadEvents should return empty NotificationEventQueue`() { + val sut = createDefaultNotificationEventPersistence() + val result = sut.loadEvents( + factory = { rawEvents -> + NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25)) + } + ) + assertThat(result.isEmpty()).isTrue() + } + + @Test + fun `after persisting NotificationEventQueue, loadEvents should return non-empty NotificationEventQueue`() { + val sut = createDefaultNotificationEventPersistence() + val notificationEventQueue = NotificationEventQueue(mutableListOf(), seenEventIds = CircularCache.create(cacheSize = 25)) + // First persist an empty queue + sut.persistEvents(notificationEventQueue) + // Add an event + notificationEventQueue.add(aSimpleNotifiableEvent()) + // Persist + // Note: is cannot work because AndroidKeyStore is not available. But we check that the code does + // not crash. + sut.persistEvents(notificationEventQueue) + sut.loadEvents( + factory = { rawEvents -> + NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25)) + } + ) + // assertThat(result.isEmpty()).isFalse() + } + + private fun createDefaultNotificationEventPersistence(): DefaultNotificationEventPersistence { + val context = RuntimeEnvironment.getApplication() + return DefaultNotificationEventPersistence(context) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt new file mode 100644 index 0000000000..09f907f50b --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.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.libraries.push.impl.notifications + +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent + +class InMemoryNotificationEventPersistence( + initialData: List = emptyList() +) : NotificationEventPersistence { + private var data: List = initialData + + override fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue { + return factory(data) + } + + override fun persistEvents(queuedEvents: NotificationEventQueue) { + data = queuedEvents.rawEvents() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt index 819a1e94a2..6276983cc9 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt @@ -25,8 +25,10 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody 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.MessageFormat import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType @@ -51,6 +53,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) class NotifiableEventResolverTest { @@ -97,6 +100,71 @@ class NotifiableEventResolverTest { assertThat(result).isEqualTo(expectedResult) } + @Test + @Config(qualifiers = "en") + fun `resolve event message with mention`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = TextMessageType(body = "Hello world", formatted = null) + ), + hasMention = true, + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Mentioned you: Hello world") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve HTML formatted event message text takes plain text version`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = TextMessageType( + body = "Hello world!", + formatted = FormattedBody( + body = "Hello world", + format = MessageFormat.HTML, + ) + ) + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Hello world") + assertThat(result).isEqualTo(expectedResult) + } + + @Test + fun `resolve incorrectly formatted event message text uses fallback`() = runTest { + val sut = createNotifiableEventResolver( + notificationResult = Result.success( + createNotificationData( + content = NotificationContent.MessageLike.RoomMessage( + senderId = A_USER_ID_2, + messageType = TextMessageType( + body = "Hello world", + formatted = FormattedBody( + body = "???Hello world!???", + format = MessageFormat.UNKNOWN, + ) + ) + ) + ) + ) + ) + val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) + val expectedResult = createNotifiableMessageEvent(body = "Hello world") + assertThat(result).isEqualTo(expectedResult) + } + @Test fun `resolve event message audio`() = runTest { val sut = createNotifiableEventResolver( @@ -430,6 +498,7 @@ class NotifiableEventResolverTest { private fun createNotificationData( content: NotificationContent, isDirect: Boolean = false, + hasMention: Boolean = false, ): NotificationData { return NotificationData( eventId = AN_EVENT_ID, @@ -444,7 +513,7 @@ class NotifiableEventResolverTest { timestamp = A_TIMESTAMP, content = content, contentUrl = null, - hasMention = false, + hasMention = hasMention, ) } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt new file mode 100644 index 0000000000..46c41454d3 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreatorTest.kt @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Build +import coil.Coil +import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi +import coil.test.FakeImageLoaderEngine +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator +import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider +import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +private const val A_TIMESTAMP = 6480L +private const val A_ROOM_AVATAR = "mxc://roomAvatar" +private const val A_USER_AVATAR_1 = "mxc://userAvatar1" +private const val A_USER_AVATAR_2 = "mxc://userAvatar2" + +@RunWith(RobolectricTestRunner::class) +class RoomGroupMessageCreatorTest { + @Test + fun `test createRoomMessage with one Event`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + imageUriString = "aUri", + ) + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: sender-name message-body", + messageCount = 1, + latestTimestamp = A_TIMESTAMP, + shouldBing = false, + ) + ) + } + + @Test + fun `test createRoomMessage with one noisy Event`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + noisy = true, + ) + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: sender-name message-body", + messageCount = 1, + latestTimestamp = A_TIMESTAMP, + shouldBing = true, + ) + ) + } + + @Test + fun `test createRoomMessage with room avatar and sender avatar android O`() { + `test createRoomMessage with room avatar and sender avatar`( + api = Build.VERSION_CODES.O, + // Only the Room avatar is loaded + expectedCoilRequests = listOf( + MediaRequestData( + source = MediaSource(url = A_ROOM_AVATAR), + kind = MediaRequestData.Kind.Thumbnail(1024) + ) + ) + ) + } + + @OptIn(ExperimentalCoilApi::class) + @Test + fun `test createRoomMessage with room avatar and sender avatar android P`() = runTest { + `test createRoomMessage with room avatar and sender avatar`( + api = Build.VERSION_CODES.P, + // Room and user avatar are loaded + expectedCoilRequests = listOf( + MediaRequestData( + source = MediaSource(url = A_USER_AVATAR_1), + kind = MediaRequestData.Kind.Thumbnail(1024) + ), + MediaRequestData( + source = MediaSource(url = A_USER_AVATAR_2), + kind = MediaRequestData.Kind.Thumbnail(1024) + ), + MediaRequestData( + source = MediaSource(url = A_ROOM_AVATAR), + kind = MediaRequestData.Kind.Thumbnail(1024) + ), + ) + ) + } + + @OptIn(ExperimentalCoilApi::class) + private fun `test createRoomMessage with room avatar and sender avatar`( + api: Int, + expectedCoilRequests: List, + ) = runTest { + val coilRequests = mutableListOf() + val engine = FakeImageLoaderEngine.Builder() + .intercept( + predicate = { + coilRequests.add(it) + true + }, + drawable = ColorDrawable(Color.BLUE) + ) + .build() + val imageLoader = ImageLoader.Builder(RuntimeEnvironment.getApplication()) + .components { add(engine) } + .build() + Coil.setImageLoader(imageLoader) + val sut = createRoomGroupMessageCreator( + sdkIntProvider = FakeBuildVersionSdkIntProvider(api) + ) + val result = sut.createRoomMessage( + currentUser = aMatrixUser( + // Some user avatar + avatarUrl = A_USER_AVATAR_1, + ), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + roomAvatarPath = A_ROOM_AVATAR, + senderAvatarPath = A_USER_AVATAR_2, + ) + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: sender-name message-body", + messageCount = 1, + latestTimestamp = A_TIMESTAMP, + shouldBing = false, + ) + ) + assertThat(coilRequests.toList()).isEqualTo(expectedCoilRequests) + } + + @Test + fun `test createRoomMessage with two Events`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP), + aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10), + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: 2 messages", + messageCount = 2, + latestTimestamp = A_TIMESTAMP + 10, + shouldBing = false, + ) + ) + } + + @Test + fun `test createRoomMessage with smart reply error`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + outGoingMessage = true, + outGoingMessageFailed = true, + ), + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "room-name: sender-name message-body", + messageCount = 0, + latestTimestamp = A_TIMESTAMP, + shouldBing = false, + ) + ) + } + + @Test + fun `test createRoomMessage for direct room`() = runTest { + val sut = createRoomGroupMessageCreator() + val result = sut.createRoomMessage( + currentUser = aMatrixUser(), + events = listOf( + aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy( + roomIsDirect = true, + ), + ), + roomId = A_ROOM_ID, + ) + val resultMetaWithoutFormatting = result.meta.copy( + summaryLine = result.meta.summaryLine.toString() + ) + assertThat(resultMetaWithoutFormatting).isEqualTo( + RoomNotification.Message.Meta( + roomId = A_ROOM_ID, + summaryLine = "sender-name: message-body", + messageCount = 1, + latestTimestamp = A_TIMESTAMP, + shouldBing = false, + ) + ) + } +} + +fun createRoomGroupMessageCreator( + sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O), +): RoomGroupMessageCreator { + val context = RuntimeEnvironment.getApplication() as Context + return RoomGroupMessageCreator( + notificationCreator = createNotificationCreator(), + bitmapLoader = NotificationBitmapLoader( + context = RuntimeEnvironment.getApplication(), + sdkIntProvider = sdkIntProvider, + ), + stringProvider = AndroidStringProvider(context.resources) + ) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt new file mode 100644 index 0000000000..2de70bb672 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.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.push.impl.notifications.factories + +import android.content.Intent +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.push.impl.intent.IntentProvider + +class FakeIntentProvider : IntentProvider { + override fun getViewRoomIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?) = Intent() + + override fun getInviteListIntent(sessionId: SessionId) = Intent() +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt new file mode 100644 index 0000000000..edf87fcd1a --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreatorTest.kt @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications.factories + +import android.app.Notification +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.meta.BuildMeta +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_THREAD_ID +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.push.impl.notifications.NotificationActionIds +import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo +import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels +import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory +import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory +import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent +import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class NotificationCreatorTest { + @Test + fun `test createDiagnosticNotification`() { + val sut = createNotificationCreator() + val result = sut.createDiagnosticNotification() + result.commonAssertions( + expectedGroup = null, + expectedCategory = NotificationCompat.CATEGORY_STATUS, + ) + } + + @Test + fun `test createFallbackNotification`() { + val sut = createNotificationCreator() + val result = sut.createFallbackNotification( + FallbackNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + description = "description", + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + timestamp = A_FAKE_TIMESTAMP, + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createSimpleEventNotification`() { + val sut = createNotificationCreator() + val result = sut.createSimpleEventNotification( + SimpleNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = false, + title = "title", + description = "description", + type = null, + timestamp = A_FAKE_TIMESTAMP, + soundName = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createSimpleEventNotification noisy`() { + val sut = createNotificationCreator() + val result = sut.createSimpleEventNotification( + SimpleNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = true, + title = "title", + description = "description", + type = null, + timestamp = A_FAKE_TIMESTAMP, + soundName = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createRoomInvitationNotification`() { + val sut = createNotificationCreator() + val result = sut.createRoomInvitationNotification( + InviteNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = false, + title = "title", + description = "description", + type = null, + timestamp = A_FAKE_TIMESTAMP, + soundName = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + roomName = "roomName", + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createRoomInvitationNotification noisy`() { + val sut = createNotificationCreator() + val result = sut.createRoomInvitationNotification( + InviteNotifiableEvent( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + editedEventId = null, + noisy = true, + title = "title", + description = "description", + type = null, + timestamp = A_FAKE_TIMESTAMP, + soundName = null, + canBeReplaced = false, + isRedacted = false, + isUpdated = false, + roomName = "roomName", + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + } + + @Test + fun `test createSummaryListNotification`() { + val sut = createNotificationCreator() + val matrixUser = aMatrixUser() + val result = sut.createSummaryListNotification( + currentUser = matrixUser, + style = null, + compatSummary = "compatSummary", + noisy = false, + lastMessageTimestamp = 123_456L, + ) + result.commonAssertions( + expectedGroup = matrixUser.userId.value, + ) + } + + @Test + fun `test createSummaryListNotification noisy`() { + val sut = createNotificationCreator() + val matrixUser = aMatrixUser() + val result = sut.createSummaryListNotification( + currentUser = matrixUser, + style = null, + compatSummary = "compatSummary", + noisy = true, + lastMessageTimestamp = 123_456L, + ) + result.commonAssertions( + expectedGroup = matrixUser.userId.value, + ) + } + + @Test + fun `test createMessagesListNotification`() { + val sut = createNotificationCreator() + aMatrixUser() + val result = sut.createMessagesListNotification( + messageStyle = NotificationCompat.MessagingStyle( + Person.Builder() + .setName("name") + .build() + ), + roomInfo = RoomEventGroupInfo( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + roomDisplayName = "roomDisplayName", + isDirect = false, + hasSmartReplyError = false, + shouldBing = false, + customSound = null, + isUpdated = false, + ), + threadId = null, + largeIcon = null, + lastMessageTimestamp = 123_456L, + tickerText = "tickerText", + ) + result.commonAssertions() + } + + @Test + fun `test createMessagesListNotification should bing and thread`() { + val sut = createNotificationCreator() + aMatrixUser() + val result = sut.createMessagesListNotification( + messageStyle = NotificationCompat.MessagingStyle( + Person.Builder() + .setName("name") + .build() + ), + roomInfo = RoomEventGroupInfo( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + roomDisplayName = "roomDisplayName", + isDirect = false, + hasSmartReplyError = false, + shouldBing = true, + customSound = null, + isUpdated = false, + ), + threadId = A_THREAD_ID, + largeIcon = null, + lastMessageTimestamp = 123_456L, + tickerText = "tickerText", + ) + result.commonAssertions() + } + + private fun Notification.commonAssertions( + expectedGroup: String? = A_SESSION_ID.value, + expectedCategory: String? = NotificationCompat.CATEGORY_MESSAGE, + ) { + assertThat(contentIntent).isNotNull() + assertThat(group).isEqualTo(expectedGroup) + assertThat(category).isEqualTo(expectedCategory) + } +} + +fun createNotificationCreator( + context: Context = RuntimeEnvironment.getApplication(), + buildMeta: BuildMeta = aBuildMeta(), + notificationChannels: NotificationChannels = createNotificationChannels() +): NotificationCreator { + return NotificationCreator( + context = context, + notificationChannels = notificationChannels, + stringProvider = FakeStringProvider("test"), + buildMeta = buildMeta, + pendingIntentFactory = PendingIntentFactory( + context, + FakeIntentProvider(), + FakeSystemClock(), + NotificationActionIds(buildMeta), + ), + markAsReadActionFactory = MarkAsReadActionFactory( + context = context, + actionIds = NotificationActionIds(buildMeta), + stringProvider = FakeStringProvider("MarkAsReadActionFactory"), + clock = FakeSystemClock(), + ), + quickReplyActionFactory = QuickReplyActionFactory( + context = context, + actionIds = NotificationActionIds(buildMeta), + stringProvider = FakeStringProvider("QuickReplyActionFactory"), + clock = FakeSystemClock(), + ), + ) +} + +fun createNotificationChannels(): NotificationChannels { + val context = RuntimeEnvironment.getApplication() + return NotificationChannels(context, FakeStringProvider("")) +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt index c046e1253f..6637b64979 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeAndroidNotificationFactory.kt @@ -17,14 +17,14 @@ package io.element.android.libraries.push.impl.notifications.fake import android.app.Notification -import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.mockk.every import io.mockk.mockk class FakeAndroidNotificationFactory { - val instance = mockk() + val instance = mockk() fun givenCreateRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification { val mockNotification = mockk() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt index 780d2abb71..44a2873465 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt @@ -77,13 +77,14 @@ fun aNotifiableMessageEvent( roomId: RoomId = A_ROOM_ID, eventId: EventId = AN_EVENT_ID, threadId: ThreadId? = null, - isRedacted: Boolean = false + isRedacted: Boolean = false, + timestamp: Long = 0, ) = NotifiableMessageEvent( sessionId = sessionId, eventId = eventId, editedEventId = null, noisy = false, - timestamp = 0, + timestamp = timestamp, senderName = "sender-name", senderId = UserId("@sending-id:domain.com"), body = "message-body", diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ElementRichTextEditorStyle.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ElementRichTextEditorStyle.kt index cc4f206cbf..3a0cf54670 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ElementRichTextEditorStyle.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ElementRichTextEditorStyle.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.textcomposer import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import io.element.android.libraries.designsystem.theme.bgSubtleTertiary import io.element.android.compound.theme.ElementTheme import io.element.android.wysiwyg.compose.RichTextEditorDefaults @@ -39,7 +40,8 @@ internal object ElementRichTextEditorStyle { m3colors.primary } else { m3colors.secondary - } + }, + lineHeight = 16.25.sp, ), cursor = RichTextEditorDefaults.cursorStyle( color = colors.iconAccentTertiary, 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 9e99163632..d9b284de6a 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 @@ -44,7 +44,6 @@ 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.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -85,7 +84,6 @@ import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState import io.element.android.libraries.ui.strings.CommonStrings -import io.element.android.wysiwyg.compose.PillStyle import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditorState import io.element.android.wysiwyg.display.TextDisplay @@ -444,8 +442,6 @@ private fun TextInput( .fillMaxWidth(), style = ElementRichTextEditorStyle.create( hasFocus = state.hasFocus - ).copy( - pill = PillStyle(Color.Red) ), resolveMentionDisplay = resolveMentionDisplay, resolveRoomMentionDisplay = resolveRoomMentionDisplay, 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 9889bf4697..ad1bbda209 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -2,13 +2,21 @@ "Smazat" "Skrýt heslo" + "Přejít dolů" "Pouze zmínky" "Ztišeno" + "Strana %1$d" "Pozastavit" "Pole pro PIN" "Přehrát" "Hlasování" "Hlasování ukončeno" + "Reagovat s %1$s" + "Reagovat s dalšími emoji" + "%1$s a %2$s přečetli" + "%1$s přečetl(a)" + "Klepnutím zobrazíte vše" + "Odstraňit reakci s %1$s" "Odeslat soubory" "Zobrazit heslo" "Zahájit hovor" @@ -31,9 +39,11 @@ "Vytvořit" "Vytvořit místnost" "Odmítnout" + "Odstranit hlasování" "Zakázat" "Hotovo" "Upravit" + "Upravit hlasování" "Povolit" "Ukončit hlasování" "Zadejte PIN" @@ -81,20 +91,22 @@ "Zahájit ověření" "Klepnutím načtete mapu" "Vyfotit" + "Klepnutím zobrazíte možnosti" "Zkusit znovu" "Zobrazit zdroj" "Ano" - "Upravit hlasování" "O aplikaci" "Zásady používání" "Pokročilá nastavení" "Analytika" + "Vzhled" "Zvuk" "Bubliny" "Záloha chatu" "Autorská práva" "Vytváření místnosti…" "Opustit místnost" + "Tmavé" "Chyba dešifrování" "Možnosti pro vývojáře" "(upraveno)" @@ -113,6 +125,7 @@ "Nainstalovat APK" "Tento Matrix identifikátor nelze najít, takže pozvánka nemusí být přijata." "Opuštění místnosti" + "Světlý" "Odkaz zkopírován do schránky" "Načítání…" "Zpráva" @@ -146,7 +159,10 @@ "Hledat někoho" "Výsledky hledání" "Zabezpečení" + "Viděno" "Odesílání…" + "Odeslání se nezdařilo" + "Odesláno" "Server není podporován" "URL serveru" "Nastavení" @@ -157,6 +173,7 @@ "Úspěch" "Návrhy" "Synchronizace" + "Systém" "Text" "Oznámení třetích stran" "Vlákno" @@ -198,6 +215,11 @@ "zadány %1$d číslice" "zadáno %1$d číslic" + + "Přečetl(a) %1$s a %2$d další" + "Přečetl(a) %1$s a %2$d další" + "Přečetl(a) %1$s a %2$d dalších" + "%1$d člen" "%1$d členové" 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 e473e6efb9..436f6a95ae 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -27,8 +27,10 @@ "Deaktivieren" "Erledigt" "Bearbeiten" + "Umfrage bearbeiten" "Aktivieren" "Umfrage beenden" + "PIN eingeben" "Passwort vergessen?" "Weiter" "Einladen" @@ -72,7 +74,6 @@ "Foto machen" "Quelle anzeigen" "Ja" - "Umfrage bearbeiten" "Über" "Nutzungsrichtlinie" "Erweiterte Einstellungen" @@ -88,6 +89,7 @@ "Bearbeitung" "* %1$s %2$s" "Verschlüsselung aktiviert" + "PIN eingeben" "Fehler" "Datei" "Datei wurde unter Downloads gespeichert" 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 ac67487989..f13e524ad6 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -9,11 +9,14 @@ "Lecture" "Sondage" "Sondage terminé" + "Lu par %1$s et %2$s" + "Lu par %1$s" "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." + "Enregistrer un message vocal." + "Arrêter l\'enregistrement" "Accepter" "Ajouter à la discussion" "Retour" @@ -33,6 +36,7 @@ "Désactiver" "Terminé" "Modifier" + "Modifier le sondage" "Activer" "Terminer le sondage" "Saisir le code PIN" @@ -80,20 +84,22 @@ "Commencer la vérification" "Cliquez pour charger la carte" "Prendre une photo" + "Appuyez pour afficher les options" "Essayer à nouveau" "Afficher la source" "Oui" - "Modifier le sondage" "À propos" "Politique d’utilisation acceptable" "Paramètres avancés" "Statistiques d’utilisation" + "Apparence" "Audio" "Bulles" "Sauvegarde des discussions" "Droits d’auteur" "Création du salon…" "Quitter le salon" + "Sombre" "Erreur de déchiffrement" "Options pour les développeurs" "(modifié)" @@ -112,9 +118,11 @@ "Installer l’APK" "Cet identifiant Matrix est introuvable, il est donc possible que l’invitation ne soit pas reçue." "Quitter le salon…" + "Clair" "Lien copié dans le presse-papiers" "Chargement…" "Message" + "Actions sur le message" "Mode d’affichage des messages" "Message supprimé" "Moderne" @@ -134,6 +142,7 @@ "Actualisation…" "En réponse à %1$s" "Signaler un problème" + "Remonter un problème" "Rapport soumis" "Éditeur de texte enrichi" "Salon" @@ -143,7 +152,10 @@ "Rechercher quelqu’un" "Résultats de la recherche" "Sécurité" + "Vu par" "Envoi en cours…" + "Échec de l\'envoi" + "Envoyé" "Serveur non pris en charge" "URL du serveur" "Paramètres" @@ -154,6 +166,7 @@ "Succès" "Suggestions" "Synchronisation" + "Système" "Texte" "Avis de tiers" "Fil de discussion" @@ -174,6 +187,7 @@ "En attente de la clé de déchiffrement" "Êtes-vous sûr de vouloir mettre fin à ce sondage ?" "Sondage : %1$s" + "Vérifier la session" "Confirmation" "Attention" "Échec de la création du permalien" @@ -193,6 +207,10 @@ "%1$d chiffre saisi" "%1$d chiffres saisis" + + "Lu par %1$s et %2$d autre" + "Lu par %1$s et %2$d autres" + "%1$d membre" "%1$d membres" 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 0b4700235b..6ad74a9086 100644 --- a/libraries/ui-strings/src/main/res/values-ru/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml @@ -2,21 +2,27 @@ "Удалить" "Скрыть пароль" + "Перейти вниз" "Только упоминания" "Звук отключен" + "Страница %1$d" "Приостановить" "Поле PIN-кода" "Воспроизвести" "Опрос" "Опрос завершен" + "Реагировать вместе с %1$s" + "Реакция с помощью эмодзи" + "Прочитано %1$s и %2$s" "Прочитано %1$s" + "Нажмите, чтобы показать все" + "Удалить реакцию с %1$s" "Отправить файлы" "Показать пароль" "Начать звонок" "Меню пользователя" "Записать голосовое сообщение." "Остановить запись" - "Прочитано %1$s и %2$s" "Разрешить" "Добавить в хронологию" "Назад" @@ -33,9 +39,11 @@ "Создать" "Создать комнату" "Отклонить" + "Удалить опрос" "Отключить" "Готово" "Редактировать" + "Редактировать опрос" "Включить" "Завершить опрос" "Введите PIN-код" @@ -87,7 +95,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 7fe0ffd9d1..0dff18fd2f 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -2,21 +2,27 @@ "Vymazať" "Skryť heslo" + "Prejsť na spodok" "Iba zmienky" "Stlmené" + "Strana %1$d" "Pozastaviť" "Pole PIN" "Prehrať" "Anketa" "Ukončená anketa" + "Reagovať s %1$s" + "Reagovať s inými emotikonmi" + "Prečítal/a %1$s a %2$s" "Prečítal/a %1$s" + "Ťuknutím zobrazíte všetko" + "Odstrániť reakciu s %1$s" "Odoslať súbory" "Zobraziť heslo" "Začať hovor" "Používateľské menu" "Nahrať hlasovú správu." "Zastaviť nahrávanie" - "Prečítal/a %1$s a %2$s" "Prijať" "Pridať na časovú os" "Späť" @@ -33,9 +39,11 @@ "Vytvoriť" "Vytvoriť miestnosť" "Odmietnuť" + "Odstrániť anketu" "Vypnúť" "Hotovo" "Upraviť" + "Upraviť anketu" "Povoliť" "Ukončiť anketu" "Zadajte PIN" @@ -87,7 +95,6 @@ "Skúste to znova" "Zobraziť zdroj" "Áno" - "Upraviť anketu" "O aplikácii" "Zásady prijateľného používania" "Pokročilé nastavenia" 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 6f6fa08892..2b3de76895 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 @@ -29,6 +29,7 @@ "停用" "完成" "編輯" + "編輯投票" "啟用" "結束投票" "忘記密碼?" @@ -74,7 +75,6 @@ "拍照" "檢視原始碼" "是" - "編輯投票" "關於" "可接受使用政策" "進階設定" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index fae900dd9c..638f4634e3 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -109,6 +109,7 @@ "Dark" "Decryption error" "Developer options" + "Direct chat" "(edited)" "Editing" "* %1$s %2$s" diff --git a/libraries/usersearch/impl/build.gradle.kts b/libraries/usersearch/impl/build.gradle.kts index 226860e86f..f1d936f1c7 100644 --- a/libraries/usersearch/impl/build.gradle.kts +++ b/libraries/usersearch/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.matrix.api) api(projects.libraries.usersearch.api) + implementation(libs.kotlinx.collections.immutable) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt index 793a9f7f58..76f29aebe5 100644 --- a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt +++ b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt @@ -25,6 +25,7 @@ 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_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,7 +38,7 @@ internal class MatrixUserListDataSourceTest { searchTerm = "test", result = Result.success( MatrixSearchUserResults( - results = listOf( + results = persistentListOf( aMatrixUserProfile(), aMatrixUserProfile(userId = A_USER_ID_2) ), 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 index b21ab48ac3..29071a9879 100644 --- 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 @@ -21,6 +21,7 @@ import android.media.MediaRecorder import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import io.element.android.libraries.core.mimetype.MimeTypes 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 @@ -50,7 +51,7 @@ object VoiceRecorderModule { VoiceFileConfig( cacheSubdir = "voice_recordings", fileExt = "ogg", - mimeType = "audio/ogg", + mimeType = MimeTypes.Ogg, ) @Provides 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 d67a44f6f3..030545a3bc 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 @@ -21,6 +21,7 @@ import android.media.MediaRecorder import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.VoiceMessageConfig +import io.element.android.libraries.core.mimetype.MimeTypes 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 @@ -85,7 +86,7 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo( VoiceRecorderState.Finished( file = File(FILE_PATH), - mimeType = "audio/ogg", + mimeType = MimeTypes.Ogg, waveform = List(100) { 1f }, duration = VoiceMessageConfig.maxVoiceMessageDuration, ) @@ -107,7 +108,7 @@ class VoiceRecorderImplTest { assertThat(awaitItem()).isEqualTo( VoiceRecorderState.Finished( file = File(FILE_PATH), - mimeType = "audio/ogg", + mimeType = MimeTypes.Ogg, waveform = List(100) { 1f }, duration = 5.seconds, ) diff --git a/libraries/voicerecorder/test/build.gradle.kts b/libraries/voicerecorder/test/build.gradle.kts index 9628825380..1dc91ab108 100644 --- a/libraries/voicerecorder/test/build.gradle.kts +++ b/libraries/voicerecorder/test/build.gradle.kts @@ -28,4 +28,5 @@ dependencies { implementation(libs.coroutines.test) implementation(libs.test.truth) + implementation(projects.libraries.core) } 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 bc8bda59d1..c4cc1da0ae 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 @@ -17,6 +17,7 @@ package io.element.android.libraries.voicerecorder.test import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import kotlinx.coroutines.flow.MutableStateFlow @@ -73,7 +74,7 @@ class FakeVoiceRecorder( null -> VoiceRecorderState.Idle else -> VoiceRecorderState.Finished( file = curRecording!!, - mimeType = "audio/ogg", + mimeType = MimeTypes.Ogg, duration = recordingDuration, waveform = waveform, ) diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 627d0eec43..b8aadaa52c 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -105,6 +105,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:cryptography:impl")) implementation(project(":libraries:voicerecorder:impl")) implementation(project(":libraries:mediaplayer:impl")) + implementation(project(":libraries:mediaviewer:impl")) } fun DependencyHandlerScope.allServicesImpl() { 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 1d74718aa9..7706cdbb5d 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 @@ -79,7 +79,6 @@ class RoomListScreen( lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters), roomLastMessageFormatter = DefaultRoomLastMessageFormatter( sp = stringProvider, - matrixClient = matrixClient, roomMembershipContentFormatter = RoomMembershipContentFormatter(matrixClient, stringProvider), profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider), stateContentFormatter = StateContentFormatter(stringProvider), diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt index a09e2a9c5e..ac6060fa8b 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt @@ -20,23 +20,19 @@ 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.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.services.appnavstate.api.NavigationState -import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.NavigationState import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class FakeAppNavigationStateService( - private val fakeAppNavigationState: MutableStateFlow = MutableStateFlow( + override val appNavigationState: MutableStateFlow = MutableStateFlow( AppNavigationState( navigationState = NavigationState.Root, isInForeground = true, ) ), ) : AppNavigationStateService { - - override val appNavigationState: StateFlow = fakeAppNavigationState - override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit override fun onLeavingSession(owner: String) = Unit diff --git a/settings.gradle.kts b/settings.gradle.kts index 0fe8ca31ee..cd3689abe3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -82,9 +82,9 @@ includeProjects(File(rootDir, "services"), ":services") // Uncomment to include the compound-android module as a local dependency so you can work on it locally. // You will also need to clone it in the specified folder. -//includeBuild("checkouts/compound-android") { +// includeBuild("checkouts/compound-android") { // dependencySubstitution { // // substitute remote dependency with local module // substitute(module("io.element.android:compound-android")).using(project(":compound")) // } -//} +// } diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Day-27_27_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Day-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.customreaction_EmojiItem_null_EmojiItem-Day-27_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_EmojiItem_null_EmojiItem-Day-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.customreaction_EmojiItem_null_EmojiItem-Night-27_28_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Night-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.customreaction_EmojiItem_null_EmojiItem-Night-27_28_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiItem_null_EmojiItem-Night-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.customreaction_EmojiPicker_null_EmojiPicker-Day-28_28_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Day-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.customreaction_EmojiPicker_null_EmojiPicker-Day-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.customreaction_EmojiPicker_null_EmojiPicker-Day-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.customreaction_EmojiPicker_null_EmojiPicker-Night-28_29_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_EmojiPicker_null_EmojiPicker-Night-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.customreaction_EmojiPicker_null_EmojiPicker-Night-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.customreaction_EmojiPicker_null_EmojiPicker-Night-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_ProgressButton_null_ProgressButton-Day-44_44_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Day-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.components.event_ProgressButton_null_ProgressButton-Day-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.event_ProgressButton_null_ProgressButton-Day-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.components.event_ProgressButton_null_ProgressButton-Night-44_45_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_ProgressButton_null_ProgressButton-Night-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.components.event_ProgressButton_null_ProgressButton-Night-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.event_ProgressButton_null_ProgressButton-Night-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.components.event_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Day-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemAudioView_null_TimelineItemAudioView-Night-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_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-30_30_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-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_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-30_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_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Day-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_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-30_31_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-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_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-30_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_TimelineItemEncryptedView_null_TimelineItemEncryptedView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Day-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_TimelineItemFileView_null_TimelineItemFileView-Day-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_TimelineItemFileView_null_TimelineItemFileView-Day-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_TimelineItemFileView_null_TimelineItemFileView-Day-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_TimelineItemFileView_null_TimelineItemFileView-Day-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_TimelineItemFileView_null_TimelineItemFileView-Day-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_TimelineItemFileView_null_TimelineItemFileView-Day-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_TimelineItemFileView_null_TimelineItemFileView-Day-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_TimelineItemFileView_null_TimelineItemFileView-Day-31_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_TimelineItemFileView_null_TimelineItemFileView-Day-33_33_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_TimelineItemFileView_null_TimelineItemFileView-Day-31_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_TimelineItemFileView_null_TimelineItemFileView-Day-33_33_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_TimelineItemFileView_null_TimelineItemFileView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Night-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_TimelineItemFileView_null_TimelineItemFileView-Night-31_32_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemFileView_null_TimelineItemFileView-Night-33_34_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_TimelineItemFileView_null_TimelineItemFileView-Night-31_32_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_TimelineItemFileView_null_TimelineItemFileView-Night-33_34_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_TimelineItemImageView_null_TimelineItemImageView-Day-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_TimelineItemImageView_null_TimelineItemImageView-Day-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_TimelineItemImageView_null_TimelineItemImageView-Day-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_TimelineItemImageView_null_TimelineItemImageView-Day-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_TimelineItemImageView_null_TimelineItemImageView-Day-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_TimelineItemImageView_null_TimelineItemImageView-Day-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_TimelineItemImageView_null_TimelineItemImageView-Day-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_TimelineItemImageView_null_TimelineItemImageView-Day-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_TimelineItemImageView_null_TimelineItemImageView-Day-32_32_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Day-34_34_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_TimelineItemImageView_null_TimelineItemImageView-Day-32_32_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_TimelineItemImageView_null_TimelineItemImageView-Day-34_34_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_TimelineItemImageView_null_TimelineItemImageView-Night-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_TimelineItemImageView_null_TimelineItemImageView-Night-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_TimelineItemImageView_null_TimelineItemImageView-Night-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_TimelineItemImageView_null_TimelineItemImageView-Night-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_TimelineItemImageView_null_TimelineItemImageView-Night-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_TimelineItemImageView_null_TimelineItemImageView-Night-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_TimelineItemImageView_null_TimelineItemImageView-Night-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_TimelineItemImageView_null_TimelineItemImageView-Night-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_TimelineItemImageView_null_TimelineItemImageView-Night-32_33_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemImageView_null_TimelineItemImageView-Night-34_35_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_TimelineItemImageView_null_TimelineItemImageView-Night-32_33_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_TimelineItemImageView_null_TimelineItemImageView-Night-34_35_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_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-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_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-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_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-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_TimelineItemInformativeView_null_TimelineItemInformativeView-Day-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_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-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_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-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_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-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_TimelineItemInformativeView_null_TimelineItemInformativeView-Night-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_TimelineItemLocationView_null_TimelineItemLocationView-Day-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_TimelineItemLocationView_null_TimelineItemLocationView-Day-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_TimelineItemLocationView_null_TimelineItemLocationView-Day-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_TimelineItemLocationView_null_TimelineItemLocationView-Day-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_TimelineItemLocationView_null_TimelineItemLocationView-Day-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_TimelineItemLocationView_null_TimelineItemLocationView-Day-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_TimelineItemLocationView_null_TimelineItemLocationView-Day-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_TimelineItemLocationView_null_TimelineItemLocationView-Day-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_TimelineItemLocationView_null_TimelineItemLocationView-Night-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_TimelineItemLocationView_null_TimelineItemLocationView-Night-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_TimelineItemLocationView_null_TimelineItemLocationView-Night-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_TimelineItemLocationView_null_TimelineItemLocationView-Night-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_TimelineItemLocationView_null_TimelineItemLocationView-Night-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_TimelineItemLocationView_null_TimelineItemLocationView-Night-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_TimelineItemLocationView_null_TimelineItemLocationView-Night-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_TimelineItemLocationView_null_TimelineItemLocationView-Night-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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Day-36_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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Day-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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Day-36_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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Day-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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Day-36_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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Day-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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Day-36_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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Day-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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Night-36_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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Night-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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Night-36_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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Night-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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Night-36_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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Night-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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Night-36_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_TimelineItemPollCreatorView_null_TimelineItemPollCreatorView-Night-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_TimelineItemPollView_null_TimelineItemPollView-Day-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_TimelineItemPollView_null_TimelineItemPollView-Day-37_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_TimelineItemPollView_null_TimelineItemPollView-Day-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_TimelineItemPollView_null_TimelineItemPollView-Day-37_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_TimelineItemPollView_null_TimelineItemPollView-Day-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_TimelineItemPollView_null_TimelineItemPollView-Day-37_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_TimelineItemPollView_null_TimelineItemPollView-Day-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_TimelineItemPollView_null_TimelineItemPollView-Day-37_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_TimelineItemPollView_null_TimelineItemPollView-Night-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_TimelineItemPollView_null_TimelineItemPollView-Night-37_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_TimelineItemPollView_null_TimelineItemPollView-Night-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_TimelineItemPollView_null_TimelineItemPollView-Night-37_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_TimelineItemPollView_null_TimelineItemPollView-Night-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_TimelineItemPollView_null_TimelineItemPollView-Night-37_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_TimelineItemPollView_null_TimelineItemPollView-Night-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_TimelineItemPollView_null_TimelineItemPollView-Night-37_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_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-37_37_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-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_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-37_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_TimelineItemRedactedView_null_TimelineItemRedactedView-Day-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_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-37_38_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-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_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-37_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_TimelineItemRedactedView_null_TimelineItemRedactedView-Night-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_TimelineItemStateView_null_TimelineItemStateView-Day-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_TimelineItemStateView_null_TimelineItemStateView-Day-40_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_TimelineItemStateView_null_TimelineItemStateView-Day-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_TimelineItemStateView_null_TimelineItemStateView-Day-40_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_TimelineItemStateView_null_TimelineItemStateView-Night-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_TimelineItemStateView_null_TimelineItemStateView-Night-40_41_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_TimelineItemStateView_null_TimelineItemStateView-Night-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_TimelineItemStateView_null_TimelineItemStateView-Night-40_41_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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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.event_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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.event_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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.event_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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.event_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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.event_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Day-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.event_TimelineItemTextView_null_TimelineItemTextView-Day-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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.event_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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.event_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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.event_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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.event_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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.event_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemTextView_null_TimelineItemTextView-Night-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.event_TimelineItemTextView_null_TimelineItemTextView-Night-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_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-40_40_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-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_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-40_40_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Day-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_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-40_41_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-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_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-40_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_TimelineItemUnknownView_null_TimelineItemUnknownView-Night-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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Day-41_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_TimelineItemVideoView_null_TimelineItemVideoView-Day-43_43_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_TimelineItemVideoView_null_TimelineItemVideoView-Day-41_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_TimelineItemVideoView_null_TimelineItemVideoView-Day-43_43_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_TimelineItemVideoView_null_TimelineItemVideoView-Night-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_TimelineItemVideoView_null_TimelineItemVideoView-Night-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-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.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-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_TimelineItemVideoView_null_TimelineItemVideoView-Night-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-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.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-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_TimelineItemVideoView_null_TimelineItemVideoView-Night-43_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.event_TimelineItemVideoView_null_TimelineItemVideoView-Night-41_42_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_TimelineItemVideoView_null_TimelineItemVideoView-Night-43_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.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-43_43_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-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.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-43_43_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Day-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.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-43_44_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-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.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-43_44_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceViewUnified_null_TimelineItemVoiceViewUnified-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-42_42_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_TimelineItemVoiceView_null_TimelineItemVoiceView-Day-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_43_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-42_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.event_TimelineItemVoiceView_null_TimelineItemVoiceView-Night-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.group_GroupHeaderView_null_GroupHeaderView-Day-45_45_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-45_45_null,NEXUS_5,1.0,en].png deleted file mode 100644 index f306954150..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-45_45_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92f929493ff8f4ea9f4a7a7b8a27901896cd00dc2509095885b41513cfe339c1 -size 25381 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-47_47_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-47_47_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1935bc644c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Day-47_47_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:910e28b2dc89b08b99b2d692d10bea6b79c85749f804842d63177d5380283006 +size 25411 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-45_46_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-45_46_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 5c756d6306..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-45_46_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f5f0e119f72f1786bdd72440a3758ddc91360899170e9c38e5b9d3edaf157f17 -size 25022 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-47_48_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-47_48_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..66c090cbb2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.group_GroupHeaderView_null_GroupHeaderView-Night-47_48_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e9af3bd8ae926ca6bf67103aad707c28e72138c564126eeb832bf589fbd2f81e +size 25033 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-46_46_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-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.html_HtmlDocument_null_HtmlDocument-Day-46_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.html_HtmlDocument_null_HtmlDocument-Day-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.html_HtmlDocument_null_HtmlDocument-Day-46_46_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-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.html_HtmlDocument_null_HtmlDocument-Day-46_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.html_HtmlDocument_null_HtmlDocument-Day-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.html_HtmlDocument_null_HtmlDocument-Day-46_46_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Day-46_46_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_HtmlDocument_null_HtmlDocument-Day-48_48_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-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.html_HtmlDocument_null_HtmlDocument-Night-46_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.html_HtmlDocument_null_HtmlDocument-Night-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.html_HtmlDocument_null_HtmlDocument-Night-46_47_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-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.html_HtmlDocument_null_HtmlDocument-Night-46_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.html_HtmlDocument_null_HtmlDocument-Night-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.html_HtmlDocument_null_HtmlDocument-Night-46_47_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_17,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_18,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_19,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_20,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_21,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.html_HtmlDocument_null_HtmlDocument-Night-48_49_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_HtmlDocument_null_HtmlDocument-Night-46_47_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_HtmlDocument_null_HtmlDocument-Night-48_49_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_SheetContent_null_SheetContent-Day-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.reactionsummary_SheetContent_null_SheetContent-Day-49_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.reactionsummary_SheetContent_null_SheetContent-Day-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.reactionsummary_SheetContent_null_SheetContent-Day-49_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.reactionsummary_SheetContent_null_SheetContent-Night-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.reactionsummary_SheetContent_null_SheetContent-Night-49_50_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_SheetContent_null_SheetContent-Night-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.reactionsummary_SheetContent_null_SheetContent-Night-49_50_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-49_49_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Day-51_51_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-49_50_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_null_ReadReceiptBottomSheet-Night-51_52_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-48_48_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-50_50_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-48_49_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.receipt_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-50_51_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.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-50_50_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-52_52_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-50_50_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-52_52_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-50_50_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-52_52_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-50_50_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Day-52_52_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-50_51_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-52_53_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-50_51_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-52_53_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-50_51_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.retrysendmenu_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-52_53_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-50_51_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_RetrySendMessageMenu_null_RetrySendMessageMenu-Night-52_53_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-51_51_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-53_53_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-51_51_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-53_53_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-51_51_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-53_53_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-51_51_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-53_53_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-51_51_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-53_53_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.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-51_51_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Day-53_53_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.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-51_52_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-53_54_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-51_52_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-53_54_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-51_52_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-53_54_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-51_52_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-53_54_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_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-51_52_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-53_54_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.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-51_52_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_EncryptedHistoryBannerView_null_EncryptedHistoryBannerView-Night-53_54_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.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-52_52_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-54_54_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-52_52_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-54_54_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-52_52_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-54_54_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-52_52_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Day-54_54_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-52_53_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-54_55_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-52_53_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-54_55_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-52_53_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-54_55_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-52_53_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_TimelineItemDaySeparatorView_null_TimelineItemDaySeparatorView-Night-54_55_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_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-53_53_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-55_55_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_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-53_53_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Day-55_55_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_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-53_54_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-55_56_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_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-53_54_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_null_TimelineItemReadMarkerView-Night-55_56_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_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-54_54_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-56_56_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_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-54_54_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Day-56_56_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_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-54_55_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-56_57_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-54_55_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_null_TimelineItemRoomBeginningView-Night-56_57_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-55_55_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-57_57_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-55_55_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Day-57_57_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-55_56_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-57_58_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-55_56_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_null_TimelineLoadingMoreIndicator-Night-57_58_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Day-22_22_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Day-22_22_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..49a9557b79 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Day-22_22_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61760159b2e8967b5349392935e88fd62353d1f8b8842b4e02090a20b34d7956 +size 8896 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Night-22_23_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Night-22_23_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af82f3930d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_null_TimelineItemGroupedEventsRowContentCollapse-Night-22_23_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65d4b7b84353c27830048ff783e1f1cf99e81b23bac46152ec17c16dcb63546c +size 8994 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-21_21_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-21_21_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a61015babf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Day-21_21_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16fb79b80d6dc1cd032bdc4383be80d6ed0f4eddf4bf42e95e28cc5c92635154 +size 14560 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-21_22_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-21_22_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..588014a61b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_null_TimelineItemGroupedEventsRowContentExpanded-Night-21_22_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f067cd464e7c14b0034fcfdbe4df3dba2bc95c05b8ac48f6788395f4631dd075 +size 14601 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-21_21_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-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_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-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_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Day-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_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-21_22_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-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_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-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_TimelineItemReactionsLayout_null_TimelineItemReactionsLayout-Night-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_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-23_23_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-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_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-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_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Day-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_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-23_24_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-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_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-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_TimelineItemReactionsViewFew_null_TimelineItemReactionsViewFew-Night-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_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-24_24_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-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_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-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_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Day-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_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-24_25_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-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_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-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_TimelineItemReactionsViewIncoming_null_TimelineItemReactionsViewIncoming-Night-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_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-25_25_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-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_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-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_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Day-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_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-25_26_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-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_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-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_TimelineItemReactionsViewOutgoing_null_TimelineItemReactionsViewOutgoing-Night-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_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-22_22_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-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_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-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_TimelineItemReactionsView_null_TimelineItemReactionsView-Day-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_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-22_23_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-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_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-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_TimelineItemReactionsView_null_TimelineItemReactionsView-Night-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_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-26_26_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-26_26_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 7c82564876..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-26_26_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:176c3dd24f1864e2690f18c8e0ff5a37f946edb8334d3aceb24593c5409aa598 -size 6968 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-28_28_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-28_28_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2ff538ae01 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Day-28_28_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bccbd7510d41c9a4645942669793ce9a8f908d54580bfb56b846258784306e06 +size 7625 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-26_27_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-26_27_null,NEXUS_5,1.0,en].png deleted file mode 100644 index b8fe62070f..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-26_27_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d52781cff2df18c5ba052551abb0ab72bc4407e6617f43362e484df2d9d9e2e -size 6863 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-28_29_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-28_29_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8f1b702891 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_TimelineItemStateEventRow_null_TimelineItemStateEventRow-Night-28_29_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:269627098efeb12c65ccf8c4808d590815b6eb2368a6c0986a541f8f50f22405 +size 7659 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-56_56_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-58_58_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-56_56_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Day-58_58_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-56_57_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-58_59_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_EventDebugInfoView_null_EventDebugInfoView-Night-56_57_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.debug_EventDebugInfoView_null_EventDebugInfoView-Night-58_59_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_0,NEXUS_5,1.0,en].png index e29d3cb464..1c79761f05 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a47b676013f086c2fec2fba9658b0bb2fe8738d6c85821818f6bfd75d0d42b4b -size 52380 +oid sha256:edf895e9fcf992db6a82c58678b441e82480d7aa3bdcd389ecc487c23105b59c +size 52505 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_1,NEXUS_5,1.0,en].png index 6befc17d3b..2a8a434a95 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:241bb08590fbf59e572f1e604912cb6ccb86cad6659c0396cfb4e8d2b7ee5c77 -size 74388 +oid sha256:a8d8205687fd1c5a7579b3c426ea9547dae4250ba4261cec36f1ef2ea588c982 +size 74730 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_12,NEXUS_5,1.0,en].png index 4c6ed0a329..54e30ae744 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b4d532e0464a6661d2a7f3924b5a0953ee92a9c5742a9b962cbef00b9ea29be -size 53897 +oid sha256:5d0f7cf75caeb2fe64452753afd9e310e645aa15be23b6726f282adb55ad54a2 +size 53987 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_13,NEXUS_5,1.0,en].png index d01dde55cd..47af42a833 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52e385af43e1340784292716de6a9efc88ac048102083ad9dc4427de7a245e66 -size 66516 +oid sha256:28266d8283d1e453832c3c88f223edc4be28d84406981f5fe87f254aecc736ca +size 66910 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_14,NEXUS_5,1.0,en].png index 50e46dbcc0..9b2728a0d9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74d7d6777f49dac448cf41687635b3fc07110167115ea676604119cb4044c540 -size 50376 +oid sha256:8029f81df796cdc60a9919a1764ee1621b154683cc1fe0c3fff3b9c91806fa7a +size 50469 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_15,NEXUS_5,1.0,en].png index 2679fa9ff8..d5a17da77b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_15,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_15,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04dc48820b1e4bb8cc4715ad95adc217f76c544bfff35cb01e2cc6174afc0099 -size 67461 +oid sha256:8ef232876e00b2c9b1448973a0fa5856f4574ce340d4035c9a3f60e41fa99fcb +size 67758 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_16,NEXUS_5,1.0,en].png index 8828064551..1c29f1f108 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_16,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_16,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ff557cc90d4459e8fa080dbfd9ae3d3594d3a4a04dda596b162b1645d0a30f3 -size 57495 +oid sha256:083c7dcc7d366172d3d6431f17697a2aa8382a7919d1cf54250ab91d5dd63874 +size 57571 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_4,NEXUS_5,1.0,en].png index 394ea8d2a1..31cf1c000f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67dca5850106994a4b56db2fe0ad6e425160171ce14071a6343cb60af7953635 -size 72112 +oid sha256:130f469d7ef167154e708a28fe1b76165cb998334d08479af46046c40c8721af +size 72592 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_5,NEXUS_5,1.0,en].png index 6edcc04fac..0b7787cbe7 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6685bfaefe430f0b5b7f9900d52e163c4c1d0394d6990c2f9edd5a20f5f06347 -size 87605 +oid sha256:8f43922435333a4b0e505be20723a8a19d62f38ff0b9d7ccdba18ea7204265d0 +size 88540 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_6,NEXUS_5,1.0,en].png index 72cfdcaac4..36e54147b1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfb62e3c9793675865908802237fdc92b04e9b0bf9a5943c036582758c83ec2c -size 74261 +oid sha256:dce3e468b31c14d39bcdfe449b837702730efa1ae42b30820c76cbdbd63b7de1 +size 74704 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_7,NEXUS_5,1.0,en].png index 9927326c78..71ba4ed07c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3f1f1e4d212ebd5e39f52b8f45a9e11223794e9bf72105df2e81a9e94d498c3 -size 105812 +oid sha256:ca184c77dc8a2d786321f03fcf3078a70ae42c347e5ac21f9b19a19bfee0da68 +size 106909 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_8,NEXUS_5,1.0,en].png index ffc15ff459..691959b549 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Day-8_8_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d5255d909ef0f25213046bd09b0778c3afc560adf267326c3dc5028fabe58dd -size 58017 +oid sha256:e39e18ebdcd6ed0fb1ca53cf46b990920d26426489c5a4ae904977299beae6e4 +size 57867 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_0,NEXUS_5,1.0,en].png index 9a4d41fca5..24131d2100 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df169a6b832a401ad205091122786d39c9f56e45505a50cacdf563a225b6f3e9 -size 50591 +oid sha256:a45cd74d0d4c030672f6ba1ad587fac5ec8dfd9aaa3906aa17748268ea74a232 +size 50883 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_1,NEXUS_5,1.0,en].png index e2c1044d72..a9a8ef4cc6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af91ab711dad1aad05a0cde3b365b9df9fa5eb607485d4d5ec8a33dabedc9bc2 -size 71351 +oid sha256:62a81a4f33a8a76a2a9864fc522b6fe15765237a96663c8ecc8aca230642a771 +size 71545 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_12,NEXUS_5,1.0,en].png index 35d465f712..b25f6dc396 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d33e25b721fa77bf6b6e032d45a284d806a36337d3a5b7962c5e701c40b6949d -size 51931 +oid sha256:37e3405d346d421faec0aa0065d220bf757dd3851bb948a716a802ca3c7664a6 +size 52189 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_13,NEXUS_5,1.0,en].png index 6fb25d0be4..32b7c884c3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_13,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_13,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b33924f77fa079a53f8f913f51ef49b48aaec9ed6068862886972fcfd77ed07 -size 63908 +oid sha256:e472556771fa5cc4253706bce70775664ba85b73566f34542aa580121583f153 +size 64185 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_14,NEXUS_5,1.0,en].png index 515e298293..140d7fd86c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_14,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_14,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53bfc38f06039a536c2cd4f2787da8c7e264ca318c9239025ab3702cbf464e7f -size 48810 +oid sha256:2aa310bd7a292b7958b4e87bceaaac1616e3b418134ad6337d84dd221663a3c9 +size 49023 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_15,NEXUS_5,1.0,en].png index 9a2a213736..7647372468 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_15,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_15,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b734310ceca9a8a72fa89b73e9e3be18f0ecd1892fc3dd6de535aaf046bed889 -size 64724 +oid sha256:0f483afa9110508d712f9ba0d6640b23e64d4ce7441506662c278e39b2213dc6 +size 64926 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_16,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_16,NEXUS_5,1.0,en].png index 1e3e9d9b6b..26d5d57080 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_16,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_16,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9839a3b13333db04ae4b4c2e7a81cfeddc7d339f43af7a0bafa1249cf774648f -size 55250 +oid sha256:6babf64bf9fa9e948f35b711bad8e96a7354593e92c8dc35cc0d38e8effa3ea7 +size 55462 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_4,NEXUS_5,1.0,en].png index 6f490214a5..0d20ccd8eb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41b0a0ab05c578b4ac9077b694338e20fa2e44854d1246b9cd324bf6708a3d69 -size 69622 +oid sha256:8c42c1e00764debb166afe9db8c5818ae53f46ab544ba1d36af5d1aefeca2ebf +size 70002 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_5,NEXUS_5,1.0,en].png index 315708fe89..c88893ce8c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccac46c978f2826eb9fd5800a681072ae7096ec871d976f191f4ef5906ec6b90 -size 82858 +oid sha256:03421bd79ed9fb2feda9ebc8112aeee8514d95d2e7d05efa3cac22076ca11de0 +size 83885 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_6,NEXUS_5,1.0,en].png index 1335bfc2c3..969d677448 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4597ef50d5281b52b37fb980ac0ffb3010b8c6a7bb747313cd1af7c10377b3f -size 71461 +oid sha256:26992624618cdf42097166cb319c477cc03b0220b7f8e1cfce43458cd0e9d6a8 +size 71941 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_7,NEXUS_5,1.0,en].png index 1c1edf0902..db334cd05c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57553bf183b04fa2a890ea6fd028664e7e17a017bc9aca1efa5b2d852c3932a8 -size 99776 +oid sha256:3d4e028e9a5991204c49d135b7d2e42dc4ae618199f6a8350e1ec6873b42fe31 +size 100807 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_8,NEXUS_5,1.0,en].png index f5444da71e..0d79825dfa 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline_TimelineView_null_TimelineView-Night-8_9_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a46ac8fa33ee07b13688ab09242f660a51cab479613808202773bcedde6d3142 -size 56390 +oid sha256:a6c73969522738d052b253abad1f8090702923f9cbcbee5935791e61bbcf40fe +size 56240 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png index 2d393d4ff2..41720c42c4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:411ea446db4c5a8be18c4f1e89cc70fced088f0a7bd7bb67216617de534fe08d -size 54458 +oid sha256:20da62ad19480eb97cb7bbb951186e0f5addf9d6883c9c0295e371b18e604ec3 +size 54318 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png index 6671cd3401..bc40dd177c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6933aeec83ec676bbb1d68d115b40541a4c689c8bb057b6b3f0ab53f8ed8352e -size 55868 +oid sha256:766c60461021cf660983e0c6886cda4dc82cac8f8259e25e14d8e782406e0ae5 +size 55873 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png index d23d404d70..91a1016fee 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:614452fdb4c43e535bd5db958dd5d52511ac69c90029cbc1c757af62a987acb8 -size 56116 +oid sha256:59fd0d904f745eeb2187651feff3ddfb49a527c260639b803f158d2044bd46cc +size 55986 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png index 5d891478b4..4d5937090e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7987a5811f4a307c10cb1b26c77f6f3a458660b1d886b3a27f1208309aab46c6 -size 51274 +oid sha256:9b8a15eefa52ddacbbb02b70c7a298ed18885ead67673764d5ffeb45c241ecaa +size 51113 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_12,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8fb84edc76 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_12,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cabc1dec0b89061db2b9f5301371b839a458e7e5bb6c8bc5b4d2fd4fcc3a179 +size 54275 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png index c586516f48..546804880d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0ad3c5d85e26f8998b2cdfd850ba0272378cf5d50ca16b87d0eefeb16e07f4c -size 56216 +oid sha256:1fd63163529af4ce35b2e5af941807a61ff9aa897f812536d67acf4c304c3caa +size 56392 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png index 95838fed45..0f770d152b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2615844f3ae65d07a204f22b202aef28f7fc964b8514a652bdff8a22c4ae5b5f -size 56160 +oid sha256:e59defd81c018d3406316519a5b50827b1515bacd6103e64cc69aeaba7eabef1 +size 56096 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png index 986c0b63f6..4f7e54bc4f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:011660f39883bf17f79e6543e80fa1362873fe8d3ce51af3dc7ea2c82375c3fd -size 52116 +oid sha256:bb37f4f0ff299692de2e8a18179994e977af77eaa5f557ccd079d1a1334fc049 +size 51965 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png index a9975f157f..c84dcd1d55 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64ad89cb794cafcb10e3ef80cd916bd823fda738aeedf6c15eeca143d8aae911 -size 52405 +oid sha256:98b2a2ca9c5678ad86e6c7268423da28d082a96c2648adb1d925e2280e9a263e +size 52914 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png index 3611778fa0..57e1f97a37 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb53a3037b698421b1976377f45d3376522fb3f413bfd9eac5fffda7b05920b1 -size 60213 +oid sha256:caf0be104b5ecccadaec47e5badeb4fa00e3c5b59cd26ec95c545a2af9930c70 +size 59948 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png index e379f7b138..640228f576 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f14de1d9ae623e62e85e151811f87da84a4932681310e4eac36ad4a9b5627ce -size 42342 +oid sha256:d8e7824d06fd9c785644800c43b7ad97951ebac62b0ba97de52265b56b06735b +size 41551 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png index 442403c840..caa286f245 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Day-0_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a87a69f2d4180bbb547b54553a3769f2997cc904be31972cd88b11c08ae96fab -size 41493 +oid sha256:e10e71cb5713efd7ffbd68664290aca47066ca3cfe03abe024359806a3bb4f1b +size 40748 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png index 6b353d0070..53c82709ba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80a197ed21dd303f251acf87769b2e83e60878bcbd81b8f968f48977838dce48 -size 52696 +oid sha256:d7b747a366dde4e06514684a8b626b00a7ebe14a8b18d93570b7bceef59f1559 +size 52659 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png index a98623e15c..f7e504518f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:773af39fed2c0d68b4a387b088ac0ca2d4b58f67e41d1825b85da1d797a62c8f -size 54054 +oid sha256:434653c48c6f38cd8edc00ae609cd796cb98088528093c907b60ed6bd915f838 +size 54150 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png index e11b04bb3a..ee3991e682 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53fe3f550f042cb2050c4e5f5b75f4729e6e8400ff0664c8140828184cfc8f4a -size 54185 +oid sha256:ad9a7c032812e2b35e67e92f23b749e14d420a04886f799cdd4821f9acc212bb +size 54141 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png index a63f72cb18..cf9b63c90c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bebf43bcf73efa4f9dfb61eef282470ba9b979fe61b66f5a102f395d90fd412 -size 45879 +oid sha256:060e29f7ed39671ecb2761b2cc123c331a6bd1629303af4681da6268fc294c7c +size 45611 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_12,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ef549a2345 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_12,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da867f9dd0e7d21419ce52e2e0eabfc26583e9dfb1523613a932394c307b3e28 +size 52621 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png index eccc61bc7a..bf478288b1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0602a650a5d1a14c32ca92e64e995d715030d6cb943d2a9edbb450fda37d0cbc -size 54747 +oid sha256:2f3d9c99ce2beef575883d84044df0bc9da74adf81a08b787d3d6ad5acdda24a +size 54821 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png index ca23bec39b..18e1f563ec 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61a23512a9a9dbe9b707d123451d8a060dcf1c8b08d7062e3f4a6849fea50c1b -size 51101 +oid sha256:891e19d4b166510eb829c64fa7f99c4537a4b5e8fc30ebe35d6cfec37bbe701d +size 50858 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png index 9561c24c63..c50c020100 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2327eb816a21cdf23c180ac62bb6e4575b0c1b1fffaef90539d6e96bbb5ef697 -size 50295 +oid sha256:016b5cf5022922abdbf9ffffc43a408b5479d3256b790aa61d4f99c4f173035e +size 50242 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png index a86825c28b..85e9e8e3eb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6baf2992489d17a5150bf7d75588605d65cb0d3e32cd09a362abd518aed6d9e3 -size 50456 +oid sha256:4ad13e228876494a9aa8e242ecd17c8c77dcb9cf5b8a063fe8d7bceb7706eac8 +size 50948 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png index 185ef7ffad..328396d1a1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95d68ccb57239f336232f3d69044640e98889ff647ee8a8bb4149d9b6a82b513 -size 54923 +oid sha256:3923e293e11238ee35d3b840ab40700fecbd315402676bf0666895c6ee176292 +size 54530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png index aa39521939..94b0fd2360 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7236bb7a5cdffda6202dd7d9c873f5b93738728fae97e0fe38397586cc09a6d -size 39093 +oid sha256:18004abe4695b0795aafba0e4527a5d000e41bdd658178ca209563bb337d1c94 +size 38178 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png index a0cafbddfe..0575d0c512 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_MessagesView_null_MessagesView-Night-0_1_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d1eae8f378020ed3fe1f024ce51bb8552c81bf22c405d7761871ac1d8dfa573 -size 38307 +oid sha256:11a28d99047da821cc54f0db2373e8256864dcb3fd380998acdb507f643e6437 +size 37405 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_2,NEXUS_5,1.0,en].png index 55252ab6fa..44fe8a5840 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00eaac72071c0b1b15b9e282c62fe82a956df63dd8246c13f2d0058b7b34a994 -size 40300 +oid sha256:d97257fbbd85d70a319795af9738a3773892d96c6b64e9e8793e693d70929e33 +size 44178 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_6,NEXUS_5,1.0,en].png index 4880eb0cf8..dd72111031 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f9afba1a28a22dface99d2359ca4a29134b3f8c9768ac71f7a492ffb07af356 -size 33406 +oid sha256:8e545dca45645bf7874d183697250ca6f8f916121e5a72a07409917a62d1dc94 +size 35440 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6f8432a888 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Day-0_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:362bcdd3b7e32613da7b9f1a6a2dc23a097e8e9b671d185220d9a243326c5b4f +size 37556 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_2,NEXUS_5,1.0,en].png index 5d12d77bda..42aa83f958 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e0c5dd90815e99d82b0dbcf6f98a8cd09d238c92a6be947fee69d43b511ec81 -size 36021 +oid sha256:80d6f893a6d3d01518c381eb5e7a2ccd261d000ee64fa8eff81a6805f24f5bf5 +size 39383 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_6,NEXUS_5,1.0,en].png index c5b3d52d79..6b1952a412 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e51bd3544012bde205e05b3b3116a0a6a682b043ac9270a91a4bff3eaa6c91fd -size 31627 +oid sha256:0ccb4cce1db9083561047204aa570727c870a2d1974534fbfc8482b3eaf4fa53 +size 33628 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e6c69623e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_CreatePollView_null_CreatePollView-Night-0_1_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fd2419fbcfbf5df4847e47416f3717c1176c5e9e5b529503916f6413c47e5cf +size 32712 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_0,NEXUS_5,1.0,en].png index 6cbb96f072..966a6e3d0d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f209aba9d799dfd6b3b947beef7f5e9ec8b6e45209579c406b41f545300ed9b -size 19562 +oid sha256:635af24c8306297ac954aa7ddb4286c36904b763e8f32acbbad841334dda8253 +size 22352 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_1,NEXUS_5,1.0,en].png index bf4bf45d77..3d6ff401a2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e23c9f4c52976030b3c397ad2e68f94bed265c147d3a40f65b6fa4d1459405d -size 17392 +oid sha256:c8b5d1c152ed4f3334fc5d6fa74546a1fa7f50dd4975f6f108028a34be0db63c +size 20248 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_2,NEXUS_5,1.0,en].png index cae6a921c3..201c88ab4a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8109e9c96d3f89ed6747b21fe326f6cdc61157b376497560ba6415d6ee592b79 -size 19932 +oid sha256:a7caaac71c2c280fc18036e725d9a64aae77e351b494440bbac6f6776658c856 +size 22544 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_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_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_3,NEXUS_5,1.0,en].png index 9c0a35fc1e..cf98fbcce8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_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_RoomMemberDetailsViewDark_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:64c553c7647dcf8e6e65e0e5f6958a1e77b67490f7f9b97670ff5ca9a5695cef -size 36807 +oid sha256:44e2a0e5c2adb1d80c2bf2abfc3ab481ef28a7dd44de8fcdca73d87933219f65 +size 38735 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_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_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_4,NEXUS_5,1.0,en].png index b6b596bd7c..ab06f857b9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_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_RoomMemberDetailsViewDark_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:40b07513dadf6339d9d46bcedc3b5e69f798262d10c9072854a9e3c07a184113 -size 28163 +oid sha256:29a24d3f9c01d866f2b3cfdff1460a47f956c2b30865f1c0c74a505a7ee40581 +size 30824 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_5,NEXUS_5,1.0,en].png index 9d9369cec3..baca4fe9e0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57428cc1459dab91ffae11e4c719e08b147d3692e24ceea3cec0997659890487 -size 20526 +oid sha256:52009b1f8c129638c82568c0d23822cbed4de45f6ce49062aa38a79dfedecbe8 +size 23041 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..03ff1f7b4b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewDark_null_RoomMemberDetailsViewDark--3_5_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68ceadfa7696c8350c45bc4c47553e8fe8c10e283ae2a3420a4192160bd97ab7 +size 22925 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_0,NEXUS_5,1.0,en].png index 52363a0f76..8da2d0200a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd6633077a2c6e45374beb32e377c813c121bac8d7312439a85808520d061676 -size 20015 +oid sha256:52dcc40a72b799510a4400662b462bba34bbb6be98a35eaa5c3edc3a671aa786 +size 23016 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_1,NEXUS_5,1.0,en].png index 831427f42b..7d7ea4300c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0ef4293fb45a3703c992616709bdd7252d939f419dfd7f3254900cdfdd4b890 -size 17771 +oid sha256:6099039f33757df49245866f6dbcca9576b216b1fe9e1f87c6c4f4e92add153c +size 20945 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_2,NEXUS_5,1.0,en].png index d31ae5a8bc..fbdffe0bb4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9019d3716d171d24a02832ec8db6885f811304a76ac6f55d0b4cba5561e51a0a -size 20419 +oid sha256:6f5586f7c60ea5959713a66cb97397c5d834b27100e1603bcbb98c430dc0873e +size 23539 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_3,NEXUS_5,1.0,en].png index 10038e1407..f76a02a956 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30dcf6770898700a1fbbd85f80cc957b90af606badcbd22ef5b9ee114142e166 -size 41500 +oid sha256:10d2ea969c9cd2bcd809e72cc56d8ae086b1b4d848636c55e9db5ef23783620d +size 43647 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_4,NEXUS_5,1.0,en].png index f81750567c..350f21eeb0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:414cfa386a1c3d97317eaa6ce4272c071420f2b3191ab56e1f4b46904febf552 -size 32491 +oid sha256:e220d275226ae8820e0119a767b2baf964f2173c430531a379e338bdf9e08adc +size 35530 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_5,NEXUS_5,1.0,en].png index 752c962509..86f5985e6f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:209f2821d99c15981b457682dc8e99036f592858404773ca3165baa66807053c -size 21020 +oid sha256:fd3964a31217ccef0ba53ab1f5341311b52746b20660461220375dcad3befcec +size 24194 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b99279289b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomdetails.impl.members.details_RoomMemberDetailsViewLight_null_RoomMemberDetailsViewLight--2_4_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1745fbfae1996203b192b07bbbf424eb129944b37b83c0203fcd05c7f332096d +size 25874 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6b7dc973bb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a475687a9b1684159dba1a08f9737c4f49a19c4b7d49cbf4d0b698403fbc84c +size 394434 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_5,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_5,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_6,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_8,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_8,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.media.viewer_MediaViewerView_null_MediaViewerView_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_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.media.viewer_MediaViewerView_null_MediaViewerView_0_null_9,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.mediaviewer.api.viewer_MediaViewerView_null_MediaViewerView_0_null_9,NEXUS_5,1.0,en].png diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 06805d49cc..88eae496a4 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -115,7 +115,8 @@ "screen_room_member_list_.*", "screen_dm_details_.*", "screen_room_notification_settings_.*", - "screen_notification_settings_edit_failed_updating_default_mode" + "screen_notification_settings_edit_failed_updating_default_mode", + "screen_start_chat_error_starting_chat" ] }, {