diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml
index 6df98f8270d..9156b57dbbf 100644
--- a/.github/workflows/generate_github_pages.yml
+++ b/.github/workflows/generate_github_pages.yml
@@ -12,7 +12,7 @@ jobs:
if: ${{ github.repository == 'element-hq/element-x-android' && ('pull_request' != github.event_name || github.event.pull_request.head.repo.full_name == github.repository) }}
steps:
- name: ⏬ Checkout with LFS
- uses: nschloe/action-cached-lfs-checkout@v1.2.2
+ uses: nschloe/action-cached-lfs-checkout@v1.2.3
- name: Use JDK 21
uses: actions/setup-java@v4
with:
diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml
index 431bf107ca8..76f53a47076 100644
--- a/.github/workflows/nightlyReports.yml
+++ b/.github/workflows/nightlyReports.yml
@@ -19,7 +19,7 @@ jobs:
if: ${{ github.repository == 'element-hq/element-x-android' }}
steps:
- name: ⏬ Checkout with LFS
- uses: nschloe/action-cached-lfs-checkout@v1.2.2
+ uses: nschloe/action-cached-lfs-checkout@v1.2.3
- name: Use JDK 21
uses: actions/setup-java@v4
diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml
index 161e2ade892..17012282e3f 100644
--- a/.github/workflows/recordScreenshots.yml
+++ b/.github/workflows/recordScreenshots.yml
@@ -24,13 +24,13 @@ jobs:
labels: Record-Screenshots
- name: ⏬ Checkout with LFS (PR)
if: github.event.label.name == 'Record-Screenshots'
- uses: nschloe/action-cached-lfs-checkout@v1.2.2
+ uses: nschloe/action-cached-lfs-checkout@v1.2.3
with:
persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch'
- uses: nschloe/action-cached-lfs-checkout@v1.2.2
+ uses: nschloe/action-cached-lfs-checkout@v1.2.3
with:
persist-credentials: false
- name: ☕️ Use JDK 21
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5c1c7d402e9..41b896d3e83 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -33,7 +33,7 @@ jobs:
sudo swapon /mnt/swapfile
sudo swapon --show
- name: ⏬ Checkout with LFS
- uses: nschloe/action-cached-lfs-checkout@v1.2.2
+ uses: nschloe/action-cached-lfs-checkout@v1.2.3
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
diff --git a/.github/workflows/validate-lfs.yml b/.github/workflows/validate-lfs.yml
index 1a70b1661e7..dd6cc5e64f8 100644
--- a/.github/workflows/validate-lfs.yml
+++ b/.github/workflows/validate-lfs.yml
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
name: Validate
steps:
- - uses: nschloe/action-cached-lfs-checkout@v1.2.2
+ - uses: nschloe/action-cached-lfs-checkout@v1.2.3
- run: |
./tools/git/validate_lfs.sh
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index c224ad564b2..bb4493707fa 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml
index ae6f5772c65..7cbf455ba23 100644
--- a/.maestro/tests/roomList/createAndDeleteRoom.yaml
+++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml
@@ -30,5 +30,6 @@ appId: ${MAESTRO_APP_ID}
# assert there's 1 member and 2 invitees
- tapOn: "Back"
- scroll
+- scroll
- tapOn: "Leave room"
- tapOn: "Leave"
diff --git a/CHANGES.md b/CHANGES.md
index 3dc07f4c661..53d6eb2e123 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,72 @@
+Changes in Element X v0.7.5 (2024-12-06)
+========================================
+
+## What's Changed
+### ✨ Features
+* Allow to set caption when uploading file and audio files, and allow adding / edit / remove caption on Event with attachment (also works on local echo) by @bmarty in https://github.com/element-hq/element-x-android/pull/3902
+* Enable all notification actions: quick reply, accept/decline invite, mark as read from notification. by @bmarty in https://github.com/element-hq/element-x-android/pull/3916
+* Video player controller by @bmarty in https://github.com/element-hq/element-x-android/pull/3959
+### 🙌 Improvements
+* change : confirm biometric before allowing biometric unlock. by @ganfra in https://github.com/element-hq/element-x-android/pull/3930
+* Hide media preprocessing by @bmarty in https://github.com/element-hq/element-x-android/pull/3943
+* changes: iterate on room create screen by @ganfra in https://github.com/element-hq/element-x-android/pull/3966
+* change : knock message supporting text display number of characters by @ganfra in https://github.com/element-hq/element-x-android/pull/3970
+* feat(design) : update send button background by @ganfra in https://github.com/element-hq/element-x-android/pull/4000
+### 🐛 Bugfixes
+* Min size for hidden media by @bmarty in https://github.com/element-hq/element-x-android/pull/3906
+* fix : use RoomMembershipObserver to close room screen when leaving by @ganfra in https://github.com/element-hq/element-x-android/pull/3887
+* fix : protect some usages of client to avoid crashes by @bmarty in https://github.com/element-hq/element-x-android/pull/3886
+* Fix long click not working on pinned events timeline by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3940
+* Element Call: display error dialog only when loading the main URL by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3962
+* Fix navigation issue when entering recovery key after navigating from the banner by @bmarty in https://github.com/element-hq/element-x-android/pull/3961
+* navigation : clear backstack when opening room from outer node by @ganfra in https://github.com/element-hq/element-x-android/pull/3984
+* fix : hide keyboard when TextComposer is removed from composition by @ganfra in https://github.com/element-hq/element-x-android/pull/3985
+* fix(room_preview) : catch all exception instead by @ganfra in https://github.com/element-hq/element-x-android/pull/3989
+* fix(room_detail) : hide room avatar preview by @ganfra in https://github.com/element-hq/element-x-android/pull/3992
+* fix(composer) : use HideKeyboardWhenDisposed only in MessagesView by @ganfra in https://github.com/element-hq/element-x-android/pull/3993
+### 🗣 Translations
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3936
+* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3975
+### Dependency upgrades
+* Update dependency io.sentry:sentry-android to v7.18.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3891
+* Update plugin sonarqube to v6 - autoclosed by @renovate in https://github.com/element-hq/element-x-android/pull/3895
+* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.64 by @renovate in https://github.com/element-hq/element-x-android/pull/3907
+* Update dependency com.autonomousapps.dependency-analysis to v2.5.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3909
+* Update dependency org.robolectric:robolectric to v4.14.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3924
+* Update dependency io.element.android:compound-android to v0.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3915
+* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.65 by @renovate in https://github.com/element-hq/element-x-android/pull/3932
+* Update media3 to v1.5.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3942
+* Update plugin ktlint to v12.1.2 by @renovate in https://github.com/element-hq/element-x-android/pull/3944
+* Update wysiwyg to v2.37.14 by @renovate in https://github.com/element-hq/element-x-android/pull/3948
+* Update mobile-dev-inc/action-maestro-cloud action to v1.9.7 by @renovate in https://github.com/element-hq/element-x-android/pull/3914
+* Update dependency com.lemonappdev:konsist to v0.17.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3947
+* deps : update rust sdk to 0.2.67 and fix breaking changes by @ganfra in https://github.com/element-hq/element-x-android/pull/3957
+* Update dependency com.lemonappdev:konsist to v0.17.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3983
+* Update plugin sonarqube to v6.0.1.5171 by @renovate in https://github.com/element-hq/element-x-android/pull/3958
+* Update dagger to v2.53 by @renovate in https://github.com/element-hq/element-x-android/pull/3986
+* Update dependency com.sigpwned:emoji4j-core to v16 by @renovate in https://github.com/element-hq/element-x-android/pull/3899
+* dependencies : update rust sdk to 0.2.68 by @ganfra in https://github.com/element-hq/element-x-android/pull/3988
+* Update plugin dependencycheck to v11.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3994
+* chore(dependencies) : update rust sdk to 0.2.69 by @ganfra in https://github.com/element-hq/element-x-android/pull/3999
+### Others
+* Send button iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/3901
+* Fix photo / video name by @bmarty in https://github.com/element-hq/element-x-android/pull/3903
+* Render edited caption. by @bmarty in https://github.com/element-hq/element-x-android/pull/3904
+* Rely on the SDK to decide if a caption is editable or not by @bmarty in https://github.com/element-hq/element-x-android/pull/3917
+* Remove AttachmentsState and use the MessagesNavigator by @bmarty in https://github.com/element-hq/element-x-android/pull/3918
+* Fix element call crash when resuming from notification by @bmarty in https://github.com/element-hq/element-x-android/pull/3926
+* Ensure that the SDK is syncing during an incoming call so that the app can cancel the notification by @bmarty in https://github.com/element-hq/element-x-android/pull/3931
+* Add feature flag to temporary disable sending caption by default in production by @bmarty in https://github.com/element-hq/element-x-android/pull/3953
+* Add timeline action item to copy caption by @bmarty in https://github.com/element-hq/element-x-android/pull/3963
+* Fix wrong name of classes and method by @bmarty in https://github.com/element-hq/element-x-android/pull/3971
+* Rework on media module by @bmarty in https://github.com/element-hq/element-x-android/pull/3967
+* Add warning when adding a caption. by @bmarty in https://github.com/element-hq/element-x-android/pull/3977
+* Do not auto-play videos. by @bmarty in https://github.com/element-hq/element-x-android/pull/3978
+* MediaViewer: iterate on design by @bmarty in https://github.com/element-hq/element-x-android/pull/3979
+* feat(crypto): Support new expected UTD causes UX + Analytics by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/3980
+* increase ringing timeout from 15 seconds to 90 seconds by @fkwp in https://github.com/element-hq/element-x-android/pull/3991
+* MediaViewer: Align title to left and move action bottom to top bar. by @bmarty in https://github.com/element-hq/element-x-android/pull/4003
+
Changes in Element X v0.7.4 (2024-11-20)
========================================
diff --git a/README.md b/README.md
index 5ae9b0c2b4d..b486d55ecb1 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ Element X Android supports many languages. You can help us to translate the app
Note that for now, we keep control on the French and German translations.
-Translations can be checked screen per screen using our tool Element X Android Gallery, available at https://element-hq.github.io/element-x-android/. Note that this page is updated every Tuesday.
+Translations can be checked screen per screen using our tool Element X Android Gallery, available at https://element-hq.github.io/element-x-android/. Note that this page is updated every Tuesday.
More instructions about translating the application can be found at [CONTRIBUTING.md](CONTRIBUTING.md#strings).
@@ -83,8 +83,11 @@ You can also come chat with the community in the Matrix [room](https://matrix.to
## Build instructions
-Just clone the project and open it in Android Studio.
-Makes sure to select the `app` configuration when building (as we also have sample apps in the project).
+Just clone the project and open it in Android Studio. Make sure to select the
+`app` configuration when building (as we also have sample apps in the project).
+
+To build against a local copy of the Rust SDK, see the [Developer
+onboarding](docs/_developer_onboarding.md#build-the-sdk-locally) instructions.
## Support
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 940e576a303..93fc8bd06e9 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -88,6 +88,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@@ -196,6 +197,10 @@ class LoggedInFlowNode @AssistedInject constructor(
) { syncState, networkStatus ->
Pair(syncState, networkStatus)
}
+ .onStart {
+ // Temporary fix to ensure that the sync is started even if the networkStatus is offline.
+ syncService.startSync()
+ }
.collect { (syncState, networkStatus) ->
Timber.d("Sync state: $syncState, network status: $networkStatus")
if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) {
diff --git a/build.gradle.kts b/build.gradle.kts
index 31edb5b08cc..942fdd37886 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -49,7 +49,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
- detektPlugins("io.nlopez.compose.rules:detekt:0.4.19")
+ detektPlugins("io.nlopez.compose.rules:detekt:0.4.22")
}
tasks.withType().configureEach {
diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md
index 9d5bdafb7a3..738bd124303 100644
--- a/docs/_developer_onboarding.md
+++ b/docs/_developer_onboarding.md
@@ -102,8 +102,8 @@ From these kotlin bindings we can generate native libs (.so files) and kotlin cl
#### Matrix Rust Component Kotlin
-To use these bindings in an android project, we need to wrap this up into an android library (as the form of an .aar file).
-This is the goal of https://github.com/matrix-org/matrix-rust-components-kotlin.
+To use these bindings in an android project, we need to wrap this up into an android library (as the form of an .aar file).
+This is the goal of https://github.com/matrix-org/matrix-rust-components-kotlin.
This repository is used for distributing kotlin releases of the Matrix Rust SDK.
It'll provide the corresponding aar and also publish them on maven.
@@ -117,41 +117,43 @@ You can also have access to the aars through the [release](https://github.com/ma
#### Build the SDK locally
-Easiest way: run the script [../tools/sdk/build_rust_sdk.sh](../tools/sdk/build_rust_sdk.sh) and just answer the questions.
-
-Legacy way:
-
-If you need to locally build the sdk-android you can use
-the [build](https://github.com/matrix-org/matrix-rust-components-kotlin/blob/main/scripts/build.sh) script.
-
-For this please check the [prerequisites](https://github.com/matrix-org/matrix-rust-components-kotlin/blob/main/README.md#prerequisites) from the repo.
-
-Checkout both [matrix-rust-sdk](https://github.com/matrix-org/matrix-rust-sdk) and [matrix-rust-components-kotlin](https://github.com/matrix-org/matrix-rust-components-kotlin) repositories
-```shell
-git clone git@github.com:matrix-org/matrix-rust-sdk.git
-git clone git@github.com:matrix-org/matrix-rust-components-kotlin.git
-```
-
-Then you can launch the build script from the matrix-rust-components-kotlin repository with the following params:
-
-- `-p` Local path to the rust-sdk repository
-- `-o` Optional output path with the expected name of the aar file. By default the aar will be located in the corresponding build/outputs/aar directory.
-- `-r` Flag to build in release mode
-- `-m` Option to select the gradle module to build. Default is sdk.
-- `-t` Option to to select an android target to build against. Default will build for all targets.
-
-So for example to build the sdk against aarch64-linux-android target and copy the generated aar to Element X project:
-
-```shell
-./scripts/build.sh -p [YOUR MATRIX RUST SDK PATH] -t aarch64-linux-android -o [YOUR element-x-android PATH]/libraries/rustsdk/matrix-rust-sdk.aar
-```
+Prerequisites:
+* Install the Android NDK (Native Development Kit). To do this from within
+ Android Studio:
+ 1. **Tools > SDK Manager**
+ 2. Click the **SDK Tools** tab.
+ 3. Select the **NDK (Side by side)** checkbox
+ 4. Click **OK**.
+ 5. Click **OK**.
+ 6. When the installation is complete, click **Finish**.
+* Install `cargo-ndk`:
+ ```
+ cargo install cargo-ndk
+ ```
+* Install the Android Rust toolchain for your machine's hardware:
+ ```
+ rustup target add aarch64-linux-android x86_64-linux-android
+ ```
+* Depending on the location of the Android SDK, you may need to set
+ `ANDROID_HOME`:
+ ```
+ export ANDROID_HOME=$HOME/android/sdk
+ ```
+
+You can then build the Rust SDK by running the script
+[`tools/sdk/build_rust_sdk.sh`](../tools/sdk/build_rust_sdk.sh) and just answering
+the questions.
+
+This will prompt you for the path to the Rust SDK, then build it and
+`matrix-rust-components-kotlin`, eventually producing an aar file at
+`./libraries/rustsdk/matrix-rust-sdk.aar`, which will be picked up
+automatically by the Element X Android build.
Troubleshooting:
- You may need to set `ANDROID_NDK_HOME` e.g `export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk`.
- If you get the error `thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', .cargo/registry/src/index.crates.io-6f17d22bba15001f/cargo-ndk-2.11.0/src/cli.rs:345:18` try updating your Cargo NDK version. In this case, 2.11.0 is too old so `cargo install cargo-ndk` to install a newer version.
- - If you get the error `Unsupported class file major version 64` try changing your JVM version. In this case, Java 20 is not supported in Gradle yet, so downgrade to an earlier version (Java 17 worked in this case).
-
-You are good to test your local rust development now!
+ - If you get the error `Unsupported class file major version `, try changing your JVM version by setting
+ `JAVA_HOME` and, if building via Android Studio, "File | Settings | Build, Execution, Deployment | Build Tools | Gradle | Gradle JDK".
### The Android project
@@ -262,7 +264,7 @@ Here are the main points:
#### Template and naming
-This documentation provides you with the steps to install and use the AS plugin for generating modules in your project.
+This documentation provides you with the steps to install and use the AS plugin for generating modules in your project.
The plugin and templates will help you quickly create new features with a standardized structure.
A. Installation
@@ -276,7 +278,7 @@ Follow these steps to install and configure the plugin and templates:
- Navigate to File/Manage IDE Settings/Import Settings
- Pick the `tmp/file_templates.zip` files
- Click on OK
-4. Configure generate-module-from-template plugin :
+4. Configure generate-module-from-template plugin :
- Navigate to AS/Settings/Tools/Module Template Settings
- Click on + / Import From File
- Pick the `tools/templates/FeatureModule.json`
@@ -296,9 +298,9 @@ Example for a new feature called RoomDetails:
5. The modules api/impl should be created under `features/roomdetails` directory.
6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
7. You can now add more Presentation classes (Events, State, StateProvider, View, Presenter) in the impl module with the `Template Presentation Classes`.
- To use it, just right click on the package where you want to generate classes, and click on `Template Presentation Classes`.
+ To use it, just right click on the package where you want to generate classes, and click on `Template Presentation Classes`.
Fill the text field with the base name of the classes, ie `RootRoomDetails` in the `root` package.
-
+
Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a
suffix `Presenter`,states MUST have a suffix `State`, etc. Also we want to have a common naming along all the modules.
diff --git a/fastlane/metadata/android/en-US/changelogs/40007020.txt b/fastlane/metadata/android/en-US/changelogs/40007020.txt
new file mode 100644
index 00000000000..4271b643da3
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40007020.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/40007030.txt b/fastlane/metadata/android/en-US/changelogs/40007030.txt
new file mode 100644
index 00000000000..120548b6e1e
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40007030.txt
@@ -0,0 +1,2 @@
+Main changes in this version: TODO.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/40007040.txt b/fastlane/metadata/android/en-US/changelogs/40007040.txt
new file mode 100644
index 00000000000..4271b643da3
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40007040.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/40007050.txt b/fastlane/metadata/android/en-US/changelogs/40007050.txt
new file mode 100644
index 00000000000..a4b397f1bb8
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40007050.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes and improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/40007060.txt b/fastlane/metadata/android/en-US/changelogs/40007060.txt
new file mode 100644
index 00000000000..1bc0b2f8e64
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40007060.txt
@@ -0,0 +1,2 @@
+Main changes in this version: media browser and bug fixes.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/features/createroom/impl/src/main/res/values-hu/translations.xml b/features/createroom/impl/src/main/res/values-hu/translations.xml
index ee935c8c60e..4a905afaa56 100644
--- a/features/createroom/impl/src/main/res/values-hu/translations.xml
+++ b/features/createroom/impl/src/main/res/values-hu/translations.xml
@@ -13,6 +13,8 @@ Ezt bármikor módosíthatja a szobabeállításokban."
"Szobahozzáférés"
"Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést"
"Csatlakozás kérése"
+ "Egyes karakterek nem engedélyezettek. Csak a betűk, a számjegyek és a következő szimbólumok támogatottak: $ & \'() * +/; =? @ [] - . _"
+ "Ez a szobacím már létezik. Próbálja meg szerkeszteni a szobacím mezőt, vagy módosítsa a szoba nevét."
"Ahhoz, hogy ez a szoba látható legyen a nyilvános szobák címtárában, meg kell adnia a szoba címét."
"Szoba címe"
"Szoba neve"
diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml
index 205f1db36a8..c60e10660b6 100644
--- a/features/createroom/impl/src/main/res/values-it/translations.xml
+++ b/features/createroom/impl/src/main/res/values-it/translations.xml
@@ -3,11 +3,22 @@
"Nuova stanza"
"Invita persone"
"Si è verificato un errore durante la creazione della stanza"
- "I messaggi in questa stanza sono cifrati. La crittografia non può essere disattivata in seguito."
- "Stanza privata (solo su invito)"
- "I messaggi non sono cifrati e chiunque può leggerli. Puoi attivare la crittografia in un secondo momento."
- "Stanza pubblica (chiunque)"
+ "Solo le persone invitate possono accedere a questa stanza. Tutti i messaggi sono cifrati end-to-end."
+ "Stanza privata"
+ "Chiunque può trovare questa stanza.
+Puoi modificarlo in qualsiasi momento nelle impostazioni della stanza."
+ "Stanza pubblica"
+ "Chiunque può entrare in questa stanza"
+ "Chiunque"
+ "Accesso alla stanza"
+ "Chiunque può chiedere di entrare nella stanza, ma un amministratore o un moderatore dovrà accettare la richiesta"
+ "Chiedi di entrare"
+ "Alcuni caratteri non sono consentiti. Sono supportate solo lettere, cifre e i seguenti simboli ! $ & \'() * +/; =? @ [] - . _"
+ "L\'indirizzo di questa stanza esiste già. Prova a modificare il campo dell\'indirizzo o a cambiare il nome della stanza"
+ "Affinché questa stanza sia visibile nell\'elenco delle stanze pubbliche, è necessario un indirizzo della stanza."
+ "Indirizzo della stanza"
"Nome stanza"
+ "Visibilità della stanza"
"Crea una stanza"
"Argomento (facoltativo)"
"Si è verificato un errore durante il tentativo di avviare una chat"
diff --git a/features/deactivation/impl/src/main/res/values-fr/translations.xml b/features/deactivation/impl/src/main/res/values-fr/translations.xml
index 3b8c5c08127..875142bc019 100644
--- a/features/deactivation/impl/src/main/res/values-fr/translations.xml
+++ b/features/deactivation/impl/src/main/res/values-fr/translations.xml
@@ -3,7 +3,7 @@
"Veuillez confirmer que vous souhaitez désactiver votre compte. Cette action ne peut pas être annulée."
"Supprimer tous mes messages"
"Attention : les futurs utilisateurs pourraient voir des conversations incomplètes."
- "La désactivation de votre compte est %1$s, cela va:"
+ "La désactivation de votre compte est %1$s, cela va :"
"irréversible"
"%1$s votre compte (vous ne pourrez plus vous reconnecter et votre identifiant ne pourra pas être réutilisé)."
"Désactiver définitivement"
diff --git a/features/joinroom/impl/src/main/res/values-fr/translations.xml b/features/joinroom/impl/src/main/res/values-fr/translations.xml
index 5e89edb1fab..c420fcfe784 100644
--- a/features/joinroom/impl/src/main/res/values-fr/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-fr/translations.xml
@@ -2,7 +2,7 @@
"Annuler la demande"
"Oui, annuler"
- "Êtes-vous sûr de vouloir annuler votre demande d’accès à ce salon?"
+ "Êtes-vous sûr de vouloir annuler votre demande d’accès à ce salon ?"
"Annuler la demande d’adhésion"
"Rejoindre"
"Demander à joindre"
@@ -13,6 +13,6 @@
"Les Spaces ne sont pas encore pris en charge"
"Cliquez ci-dessous et un administrateur sera prévenu. Une fois votre demande approuvée, pour pourrez rejoindre la discussion."
"Vous devez être un membre du salon pour pouvoir lire l’historique des messages."
- "Vous souhaitez rejoindre ce salon?"
+ "Vous souhaitez rejoindre ce salon ?"
"La prévisualisation n’est pas disponible"
diff --git a/features/joinroom/impl/src/main/res/values-it/translations.xml b/features/joinroom/impl/src/main/res/values-it/translations.xml
index 55af91d9d70..16202e2f87a 100644
--- a/features/joinroom/impl/src/main/res/values-it/translations.xml
+++ b/features/joinroom/impl/src/main/res/values-it/translations.xml
@@ -1,7 +1,14 @@
+ "Cancella richiesta"
+ "Sì, annulla"
+ "Sei sicuro di voler annullare la tua richiesta di accesso a questa stanza?"
+ "Annulla la richiesta di accesso"
"Entra nella stanza"
"Bussa per partecipare"
+ "Messaggio (opzionale)"
+ "Riceverai un invito a entrare nella stanza se la tua richiesta viene accettata."
+ "Richiesta di accesso inviata"
"%1$s non supporta ancora gli spazi. Puoi accedere agli spazi sul web."
"Gli spazi non sono ancora supportati"
"Clicca sul pulsante qui sotto e un amministratore della stanza riceverà una notifica. Potrai partecipare alla conversazione una volta approvato."
diff --git a/features/knockrequests/api/build.gradle.kts b/features/knockrequests/api/build.gradle.kts
new file mode 100644
index 00000000000..c3eca7567c9
--- /dev/null
+++ b/features/knockrequests/api/build.gradle.kts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.features.knockrequests.api"
+}
+
+dependencies {
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+}
diff --git a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt
new file mode 100644
index 00000000000..86483aee706
--- /dev/null
+++ b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.api.banner
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+interface KnockRequestsBannerRenderer {
+ @Composable
+ fun View(modifier: Modifier, onViewRequestsClick: () -> Unit)
+}
diff --git a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/list/KnockRequestsListEntryPoint.kt b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/list/KnockRequestsListEntryPoint.kt
new file mode 100644
index 00000000000..0215b5cde90
--- /dev/null
+++ b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/list/KnockRequestsListEntryPoint.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.api.list
+
+import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
+
+interface KnockRequestsListEntryPoint : SimpleFeatureEntryPoint
diff --git a/features/knockrequests/impl/build.gradle.kts b/features/knockrequests/impl/build.gradle.kts
new file mode 100644
index 00000000000..2664528d746
--- /dev/null
+++ b/features/knockrequests/impl/build.gradle.kts
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+import extension.setupAnvil
+
+plugins {
+ id("io.element.android-compose-library")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "io.element.android.features.knockrequests.impl"
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+}
+
+setupAnvil()
+
+dependencies {
+ api(projects.features.knockrequests.api)
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.architecture)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrixui)
+ implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.featureflag.api)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.coroutines.test)
+ testImplementation(libs.molecule.runtime)
+ testImplementation(libs.test.robolectric)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.turbine)
+ testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.tests.testutils)
+ testImplementation(libs.androidx.compose.ui.test.junit)
+ testImplementation(projects.libraries.featureflag.test)
+ testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt
new file mode 100644
index 00000000000..9fce2f21732
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/DefaultKnockRequestsBannerRenderer.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.banner
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer
+import io.element.android.libraries.di.RoomScope
+import javax.inject.Inject
+
+@ContributesBinding(RoomScope::class)
+class DefaultKnockRequestsBannerRenderer @Inject constructor(
+ private val presenter: KnockRequestsBannerPresenter,
+) : KnockRequestsBannerRenderer {
+ @Composable
+ override fun View(modifier: Modifier, onViewRequestsClick: () -> Unit) {
+ val state = presenter.present()
+ KnockRequestsBannerView(
+ state = state,
+ onViewRequestsClick = onViewRequestsClick,
+ )
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt
new file mode 100644
index 00000000000..14239d93ef3
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.banner
+
+sealed interface KnockRequestsBannerEvents {
+ data object AcceptSingleRequest : KnockRequestsBannerEvents
+ data object Dismiss : KnockRequestsBannerEvents
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
new file mode 100644
index 00000000000..f155cdb4a35
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.banner
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
+import io.element.android.features.knockrequests.impl.data.KnockRequestsService
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.coroutine.mapState
+import io.element.android.libraries.core.extensions.firstIfSingle
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val ACCEPT_ERROR_DISPLAY_DURATION = 1500L
+
+class KnockRequestsBannerPresenter @Inject constructor(
+ private val knockRequestsService: KnockRequestsService,
+ private val appCoroutineScope: CoroutineScope,
+) : Presenter {
+ @Composable
+ override fun present(): KnockRequestsBannerState {
+ val knockRequests by remember {
+ knockRequestsService.knockRequestsFlow.mapState { knockRequests ->
+ knockRequests.dataOrNull().orEmpty()
+ .filter { !it.isSeen }
+ .toImmutableList()
+ }
+ }.collectAsState()
+
+ val permissions by knockRequestsService.permissionsFlow.collectAsState()
+ val showAcceptError = remember { mutableStateOf(false) }
+
+ val shouldShowBanner by remember {
+ derivedStateOf {
+ permissions.canHandle && knockRequests.isNotEmpty()
+ }
+ }
+
+ fun handleEvents(event: KnockRequestsBannerEvents) {
+ when (event) {
+ is KnockRequestsBannerEvents.AcceptSingleRequest -> {
+ appCoroutineScope.acceptSingleKnockRequest(
+ knockRequests = knockRequests,
+ displayAcceptError = showAcceptError,
+ )
+ }
+ is KnockRequestsBannerEvents.Dismiss -> {
+ appCoroutineScope.launch {
+ knockRequestsService.markAllKnockRequestsAsSeen()
+ }
+ }
+ }
+ }
+
+ return KnockRequestsBannerState(
+ knockRequests = knockRequests,
+ displayAcceptError = showAcceptError.value,
+ canAccept = permissions.canAccept,
+ isVisible = shouldShowBanner,
+ eventSink = ::handleEvents,
+ )
+ }
+
+ private fun CoroutineScope.acceptSingleKnockRequest(
+ knockRequests: List,
+ displayAcceptError: MutableState,
+ ) = launch {
+ val knockRequest = knockRequests.firstIfSingle()
+ if (knockRequest != null) {
+ knockRequestsService.acceptKnockRequest(knockRequest, optimistic = true)
+ .onFailure {
+ displayAcceptError.value = true
+ delay(ACCEPT_ERROR_DISPLAY_DURATION)
+ displayAcceptError.value = false
+ }
+ }
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt
new file mode 100644
index 00000000000..80d662bc5b6
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.banner
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import io.element.android.features.knockrequests.impl.R
+import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
+import io.element.android.libraries.core.extensions.firstIfSingle
+import kotlinx.collections.immutable.ImmutableList
+
+data class KnockRequestsBannerState(
+ val isVisible: Boolean,
+ val knockRequests: ImmutableList,
+ val displayAcceptError: Boolean,
+ val canAccept: Boolean,
+ val eventSink: (KnockRequestsBannerEvents) -> Unit,
+) {
+ val subtitle = knockRequests.firstIfSingle()?.userId?.value
+ val reason = knockRequests.firstIfSingle()?.reason
+
+ @Composable
+ fun formattedTitle(): String {
+ return when (knockRequests.size) {
+ 0 -> ""
+ 1 -> stringResource(R.string.screen_room_single_knock_request_title, knockRequests.first().getBestName())
+ else -> {
+ val firstRequest = knockRequests.first()
+ val otherRequestsCount = knockRequests.size - 1
+ pluralStringResource(
+ id = R.plurals.screen_room_multiple_knock_requests_title,
+ count = otherRequestsCount,
+ firstRequest.getBestName(),
+ otherRequestsCount
+ )
+ }
+ }
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt
new file mode 100644
index 00000000000..0324239fd92
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.banner
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
+import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable
+import kotlinx.collections.immutable.toImmutableList
+
+class KnockRequestsBannerStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aKnockRequestsBannerState(),
+ aKnockRequestsBannerState(
+ knockRequests = listOf(
+ aKnockRequestPresentable(
+ reason = "A very long reason that should probably be truncated, " +
+ "but could be also expanded so you can see it over the lines, wow," +
+ "very amazing reason, I know, right, I'm so good at writing reasons."
+ )
+ )
+ ),
+ aKnockRequestsBannerState(
+ knockRequests = listOf(
+ aKnockRequestPresentable(),
+ aKnockRequestPresentable(displayName = "Alice")
+ )
+ ),
+ aKnockRequestsBannerState(
+ knockRequests = listOf(
+ aKnockRequestPresentable(),
+ aKnockRequestPresentable(displayName = "Alice"),
+ aKnockRequestPresentable(displayName = "Bob"),
+ aKnockRequestPresentable(displayName = "Charlie")
+ )
+ ),
+ aKnockRequestsBannerState(
+ canAccept = false
+ ),
+ aKnockRequestsBannerState(
+ displayAcceptError = true
+ ),
+ aKnockRequestsBannerState(
+ knockRequests = listOf(
+ aKnockRequestPresentable(
+ displayName = "A_very_long_display_name_so_that_the_text_can_be_displayed_on_multiple_lines"
+ )
+ )
+ ),
+ )
+}
+
+fun aKnockRequestsBannerState(
+ knockRequests: List = listOf(aKnockRequestPresentable()),
+ displayAcceptError: Boolean = false,
+ canAccept: Boolean = true,
+ isVisible: Boolean = true,
+ eventSink: (KnockRequestsBannerEvents) -> Unit = {}
+) = KnockRequestsBannerState(
+ knockRequests = knockRequests.toImmutableList(),
+ displayAcceptError = displayAcceptError,
+ canAccept = canAccept,
+ isVisible = isVisible,
+ eventSink = eventSink,
+)
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
new file mode 100644
index 00000000000..d029b809065
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.banner
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.knockrequests.impl.R
+import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
+import io.element.android.libraries.designsystem.components.async.AsyncIndicator
+import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
+import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
+import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.ButtonSize
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.OutlinedButton
+import io.element.android.libraries.designsystem.theme.components.Surface
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+
+private const val MAX_AVATAR_COUNT = 3
+
+@Composable
+fun KnockRequestsBannerView(
+ state: KnockRequestsBannerState,
+ onViewRequestsClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier = modifier) {
+ AnimatedVisibility(
+ visible = state.isVisible,
+ enter = expandVertically(),
+ exit = shrinkVertically(),
+ ) {
+ Surface(
+ shape = MaterialTheme.shapes.small,
+ color = ElementTheme.colors.bgCanvasDefaultLevel1,
+ shadowElevation = 24.dp,
+ modifier = Modifier.padding(16.dp),
+ ) {
+ KnockRequestsBannerContent(
+ state = state,
+ onViewRequestsClick = onViewRequestsClick,
+ )
+ }
+ }
+ KnockRequestsAcceptErrorView(displayError = state.displayAcceptError)
+ }
+}
+
+@Composable
+private fun KnockRequestsAcceptErrorView(
+ displayError: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ val asyncIndicatorState = rememberAsyncIndicatorState()
+ AsyncIndicatorHost(modifier = modifier.statusBarsPadding(), state = asyncIndicatorState)
+ LaunchedEffect(displayError) {
+ if (displayError) {
+ asyncIndicatorState.enqueue {
+ AsyncIndicator.Custom(text = stringResource(CommonStrings.error_unknown))
+ }
+ } else {
+ asyncIndicatorState.clear()
+ }
+ }
+}
+
+@Composable
+private fun KnockRequestsBannerContent(
+ state: KnockRequestsBannerState,
+ onViewRequestsClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ fun onDismissClick() {
+ state.eventSink(KnockRequestsBannerEvents.Dismiss)
+ }
+
+ fun onAcceptClick() {
+ state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
+ }
+
+ Column(
+ modifier
+ .fillMaxWidth()
+ .padding(all = 16.dp)
+ ) {
+ Row {
+ KnockRequestAvatarView(
+ state.knockRequests,
+ modifier = Modifier.padding(top = 2.dp),
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = state.formattedTitle(),
+ style = ElementTheme.typography.fontBodyMdMedium,
+ color = MaterialTheme.colorScheme.primary,
+ textAlign = TextAlign.Start,
+ )
+ if (state.subtitle != null) {
+ Text(
+ text = state.subtitle,
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = MaterialTheme.colorScheme.secondary,
+ textAlign = TextAlign.Start,
+ )
+ }
+ }
+ Spacer(modifier = Modifier.width(4.dp))
+ Icon(
+ modifier = Modifier.clickable(onClick = ::onDismissClick),
+ imageVector = CompoundIcons.Close(),
+ contentDescription = stringResource(CommonStrings.action_close)
+ )
+ }
+ val reason = state.reason
+ if (!reason.isNullOrEmpty()) {
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = state.reason,
+ color = ElementTheme.colors.textPrimary,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ if (state.knockRequests.size > 1) {
+ Button(
+ text = stringResource(R.string.screen_room_multiple_knock_requests_view_all_button_title),
+ onClick = onViewRequestsClick,
+ size = ButtonSize.MediumLowPadding,
+ modifier = Modifier.weight(1f),
+ )
+ } else {
+ OutlinedButton(
+ text = stringResource(R.string.screen_room_single_knock_request_view_button_title),
+ onClick = onViewRequestsClick,
+ size = ButtonSize.MediumLowPadding,
+ modifier = Modifier.weight(1f),
+ )
+ if (state.canAccept) {
+ Button(
+ text = stringResource(R.string.screen_room_single_knock_request_accept_button_title),
+ onClick = ::onAcceptClick,
+ size = ButtonSize.MediumLowPadding,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun KnockRequestAvatarView(
+ knockRequests: ImmutableList,
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier) {
+ when (knockRequests.size) {
+ 0 -> Unit
+ 1 -> Avatar(knockRequests.first().getAvatarData(AvatarSize.KnockRequestBanner))
+ else -> KnockRequestAvatarListView(knockRequests)
+ }
+ }
+}
+
+@Composable
+private fun KnockRequestAvatarListView(
+ knockRequests: ImmutableList,
+ modifier: Modifier = Modifier,
+) {
+ val avatarSize = AvatarSize.KnockRequestBanner.dp
+ Box(
+ modifier = modifier,
+ ) {
+ knockRequests
+ .take(MAX_AVATAR_COUNT)
+ .reversed()
+ .let { smallReversedList ->
+ val lastItemIndex = smallReversedList.size - 1
+ smallReversedList.forEachIndexed { index, knockRequest ->
+ Avatar(
+ modifier = Modifier
+ .padding(start = avatarSize / 2 * (lastItemIndex - index))
+ .graphicsLayer {
+ compositingStrategy = CompositingStrategy.Offscreen
+ }
+ .drawWithContent {
+ // Draw content and clear the pixels for the avatar on the left.
+ drawContent()
+ if (index < lastItemIndex) {
+ drawCircle(
+ color = Color.Black,
+ center = Offset(
+ x = 0f,
+ y = size.height / 2,
+ ),
+ radius = avatarSize.toPx() / 2,
+ blendMode = BlendMode.Clear,
+ )
+ }
+ }
+ .size(size = avatarSize)
+ .padding(2.dp),
+ avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+@PreviewsDayNight
+internal fun KnockRequestsBannerViewPreview(@PreviewParameter(KnockRequestsBannerStateProvider::class) state: KnockRequestsBannerState) = ElementPreview {
+ KnockRequestsBannerView(
+ state = state,
+ onViewRequestsClick = {},
+ )
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt
new file mode 100644
index 00000000000..cfecb8355e2
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.data
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UserId
+
+fun aKnockRequestPresentable(
+ eventId: EventId = EventId("\$eventId"),
+ userId: UserId = UserId("@jacob_ross:example.com"),
+ displayName: String? = "Jacob Ross",
+ avatarUrl: String? = null,
+ reason: String? = "Hi, I would like to get access to this room please.",
+ formattedDate: String? = "20 Nov 2024",
+) = object : KnockRequestPresentable {
+ override val eventId: EventId = eventId
+ override val userId: UserId = userId
+ override val displayName: String? = displayName
+ override val avatarUrl: String? = avatarUrl
+ override val reason: String? = reason
+ override val formattedDate: String? = formattedDate
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt
new file mode 100644
index 00000000000..658717d48b6
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.data
+
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.api.room.powerlevels.canBan
+import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
+import io.element.android.libraries.matrix.api.room.powerlevels.canKick
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+data class KnockRequestPermissions(
+ val canAccept: Boolean,
+ val canDecline: Boolean,
+ val canBan: Boolean,
+) {
+ val canHandle = canAccept || canDecline || canBan
+}
+
+fun MatrixRoom.knockRequestPermissionsFlow(): Flow {
+ return syncUpdateFlow.map {
+ val canAccept = canInvite().getOrDefault(false)
+ val canDecline = canKick().getOrDefault(false)
+ val canBan = canBan().getOrDefault(false)
+ KnockRequestPermissions(canAccept, canDecline, canBan)
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt
new file mode 100644
index 00000000000..5d45281d8a2
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.data
+
+import androidx.compose.runtime.Immutable
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UserId
+
+@Immutable
+interface KnockRequestPresentable {
+ val eventId: EventId
+ val userId: UserId
+ val displayName: String?
+ val avatarUrl: String?
+ val reason: String?
+ val formattedDate: String?
+
+ fun getAvatarData(size: AvatarSize) = AvatarData(
+ id = userId.value,
+ name = displayName,
+ url = avatarUrl,
+ size = size,
+ )
+
+ fun getBestName(): String {
+ return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt
new file mode 100644
index 00000000000..f1df84beab5
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.data
+
+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.room.knock.KnockRequest
+
+class KnockRequestWrapper(
+ private val inner: KnockRequest,
+ dateFormatter: (Long?) -> String? = { null }
+) : KnockRequestPresentable {
+ override val eventId: EventId = inner.eventId
+ override val userId: UserId = inner.userId
+ override val displayName: String? = inner.displayName
+ override val avatarUrl: String? = inner.avatarUrl
+ override val reason: String? = inner.reason?.trim()
+ override val formattedDate: String? = dateFormatter(inner.timestamp)
+
+ val isSeen: Boolean = inner.isSeen
+
+ suspend fun accept(): Result = inner.accept()
+
+ suspend fun decline(reason: String?): Result = inner.decline(reason)
+
+ suspend fun declineAndBan(reason: String?): Result = inner.declineAndBan(reason)
+
+ suspend fun markAsSeen(): Result = inner.markAsSeen()
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt
new file mode 100644
index 00000000000..0880233ff6b
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.data
+
+sealed class KnockRequestsException : Exception() {
+ data object AcceptAllPartiallyFailed : KnockRequestsException()
+ data object KnockRequestNotFound : KnockRequestsException()
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt
new file mode 100644
index 00000000000..1c1a17767f9
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.data
+
+import com.squareup.anvil.annotations.ContributesTo
+import dagger.Module
+import dagger.Provides
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.featureflag.api.FeatureFlagService
+import io.element.android.libraries.featureflag.api.FeatureFlags
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+
+@Module
+@ContributesTo(RoomScope::class)
+object KnockRequestsModule {
+ @Provides
+ @SingleIn(RoomScope::class)
+ fun knockRequestsService(room: MatrixRoom, featureFlagService: FeatureFlagService): KnockRequestsService {
+ return KnockRequestsService(
+ knockRequestsFlow = room.knockRequestsFlow,
+ permissionsFlow = room.knockRequestPermissionsFlow(),
+ isKnockFeatureEnabledFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock),
+ coroutineScope = room.roomCoroutineScope
+ )
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt
new file mode 100644
index 00000000000..fb088213875
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.data
+
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.room.knock.KnockRequest
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.getAndUpdate
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.supervisorScope
+
+class KnockRequestsService(
+ knockRequestsFlow: Flow>,
+ permissionsFlow: Flow,
+ isKnockFeatureEnabledFlow: Flow,
+ coroutineScope: CoroutineScope,
+) {
+ // Keep track of the knock requests that have been handled, so we don't have to wait for sync to remove them.
+ private val handledKnockRequestIds = MutableStateFlow>(emptySet())
+
+ val knockRequestsFlow = combine(
+ isKnockFeatureEnabledFlow,
+ knockRequestsFlow,
+ handledKnockRequestIds,
+ ) { isKnockEnabled, knockRequests, handledKnockIds ->
+ if (!isKnockEnabled) {
+ AsyncData.Success(persistentListOf())
+ } else {
+ val presentableKnockRequests = knockRequests
+ .filter { it.eventId !in handledKnockIds }
+ .map { inner -> KnockRequestWrapper(inner) }
+ .toImmutableList()
+ AsyncData.Success(presentableKnockRequests)
+ }
+ }.stateIn(coroutineScope, SharingStarted.Lazily, AsyncData.Loading())
+
+ val permissionsFlow = permissionsFlow.stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.Lazily,
+ initialValue = KnockRequestPermissions(canAccept = false, canDecline = false, canBan = false)
+ )
+
+ private fun knockRequestsList() = knockRequestsFlow.value.dataOrNull().orEmpty()
+
+ private fun getKnockRequestById(eventId: EventId): KnockRequestWrapper? {
+ return knockRequestsList().find { it.eventId == eventId }
+ }
+
+ /**
+ * Accept a knock request.
+ * @param knockRequest The knock request to accept.
+ * @param optimistic If true, the request will be marked as handled before the server responds.
+ */
+ suspend fun acceptKnockRequest(knockRequest: KnockRequestPresentable, optimistic: Boolean = false): Result {
+ val wrapped = getKnockRequestById(knockRequest.eventId) ?: return knockRequestNotFoundResult()
+ return handleKnockRequest(wrapped, optimistic) { accept() }
+ }
+
+ /**
+ * Decline a knock request.
+ * @param knockRequest The knock request to decline.
+ * @param optimistic If true, the request will be marked as handled before the server responds.
+ */
+ suspend fun declineKnockRequest(knockRequest: KnockRequestPresentable, optimistic: Boolean = false): Result {
+ val wrapped = getKnockRequestById(knockRequest.eventId) ?: return knockRequestNotFoundResult()
+ return handleKnockRequest(wrapped, optimistic) { decline(null) }
+ }
+
+ /**
+ * Decline a knock request by banning the user.
+ * @param knockRequest The knock request to decline.
+ * @param optimistic If true, the request will be marked as handled before the server responds.
+ */
+ suspend fun declineAndBanKnockRequest(knockRequest: KnockRequestPresentable, optimistic: Boolean = false): Result {
+ val wrapped = getKnockRequestById(knockRequest.eventId) ?: return knockRequestNotFoundResult()
+ return handleKnockRequest(wrapped, optimistic) { declineAndBan(null) }
+ }
+
+ /**
+ * Accept all currently known knock requests.
+ * @param optimistic If true, the requests will be marked as handled before the server responds.
+ */
+ suspend fun acceptAllKnockRequests(optimistic: Boolean = false): Result = supervisorScope {
+ val results = knockRequestsList()
+ .map { knockRequest ->
+ async {
+ acceptKnockRequest(knockRequest, optimistic = optimistic)
+ }
+ }
+ .awaitAll()
+ if (results.all { it.isSuccess }) {
+ Result.success(Unit)
+ } else {
+ Result.failure(KnockRequestsException.AcceptAllPartiallyFailed)
+ }
+ }
+
+ /**
+ * Mark all currently known knock requests as seen.
+ */
+ suspend fun markAllKnockRequestsAsSeen() = supervisorScope {
+ knockRequestsList()
+ .map { knockRequest ->
+ async { knockRequest.markAsSeen() }
+ }
+ .awaitAll()
+ }
+
+ private suspend fun handleKnockRequest(
+ knockRequest: KnockRequestWrapper,
+ optimistic: Boolean,
+ action: suspend (KnockRequestWrapper.() -> Result)
+ ): Result {
+ if (optimistic) {
+ handledKnockRequestIds.getAndUpdate { it + knockRequest.eventId }
+ }
+ return action(knockRequest)
+ .onFailure {
+ if (optimistic) {
+ handledKnockRequestIds.getAndUpdate { it - knockRequest.eventId }
+ }
+ }
+ .onSuccess {
+ if (!optimistic) {
+ handledKnockRequestIds.getAndUpdate { it + knockRequest.eventId }
+ }
+ }
+ }
+}
+
+private fun knockRequestNotFoundResult() = Result.failure(KnockRequestsException.KnockRequestNotFound)
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt
new file mode 100644
index 00000000000..c685f1cf37d
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/DefaultKnockRequestsListEntryPoint.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultKnockRequestsListEntryPoint @Inject constructor() : KnockRequestsListEntryPoint {
+ override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
+ return parentNode.createNode(buildContext)
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt
new file mode 100644
index 00000000000..23b1025ce2f
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
+
+sealed interface KnockRequestsListEvents {
+ data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
+ data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
+ data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
+ data object AcceptAll : KnockRequestsListEvents
+ data object ResetCurrentAction : KnockRequestsListEvents
+ data object RetryCurrentAction : KnockRequestsListEvents
+ data object ConfirmCurrentAction : KnockRequestsListEvents
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt
new file mode 100644
index 00000000000..ce8d6028618
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListNode.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.di.RoomScope
+
+@ContributesNode(RoomScope::class)
+class KnockRequestsListNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ private val presenter: KnockRequestsListPresenter,
+) : Node(buildContext, plugins = plugins) {
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ KnockRequestsListView(
+ state = state,
+ onBackClick = ::navigateUp,
+ modifier = modifier
+ )
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt
new file mode 100644
index 00000000000..6ea13f16a1d
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import io.element.android.features.knockrequests.impl.data.KnockRequestsService
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.runUpdatingState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class KnockRequestsListPresenter @Inject constructor(
+ private val knockRequestsService: KnockRequestsService,
+) : Presenter {
+ @Composable
+ override fun present(): KnockRequestsListState {
+ val asyncAction = remember { mutableStateOf>(AsyncAction.Uninitialized) }
+ var currentAction by remember { mutableStateOf(KnockRequestsAction.None) }
+
+ val permissions by knockRequestsService.permissionsFlow.collectAsState()
+ val knockRequests by knockRequestsService.knockRequestsFlow.collectAsState()
+
+ val coroutineScope = rememberCoroutineScope()
+
+ fun handleEvents(event: KnockRequestsListEvents) {
+ when (event) {
+ KnockRequestsListEvents.AcceptAll -> {
+ currentAction = KnockRequestsAction.AcceptAll
+ }
+ is KnockRequestsListEvents.Accept -> {
+ currentAction = KnockRequestsAction.Accept(event.knockRequest)
+ }
+ is KnockRequestsListEvents.Decline -> {
+ currentAction = KnockRequestsAction.Decline(event.knockRequest)
+ }
+ is KnockRequestsListEvents.DeclineAndBan -> {
+ currentAction = KnockRequestsAction.DeclineAndBan(event.knockRequest)
+ }
+ KnockRequestsListEvents.ResetCurrentAction -> {
+ asyncAction.value = AsyncAction.Uninitialized
+ currentAction = KnockRequestsAction.None
+ }
+ KnockRequestsListEvents.RetryCurrentAction -> {
+ coroutineScope.executeAction(currentAction, asyncAction, isActionConfirmed = true)
+ }
+ KnockRequestsListEvents.ConfirmCurrentAction -> {
+ coroutineScope.executeAction(currentAction, asyncAction, isActionConfirmed = true)
+ }
+ }
+ }
+ LaunchedEffect(currentAction) {
+ executeAction(currentAction, asyncAction, isActionConfirmed = false)
+ }
+
+ return KnockRequestsListState(
+ knockRequests = knockRequests,
+ currentAction = currentAction,
+ permissions = permissions,
+ asyncAction = asyncAction.value,
+ eventSink = ::handleEvents
+ )
+ }
+
+ private fun CoroutineScope.executeAction(
+ currentAction: KnockRequestsAction,
+ asyncAction: MutableState>,
+ isActionConfirmed: Boolean,
+ ) = launch {
+ when (currentAction) {
+ is KnockRequestsAction.Accept -> {
+ runUpdatingState(asyncAction) {
+ knockRequestsService.acceptKnockRequest(currentAction.knockRequest)
+ }
+ }
+ is KnockRequestsAction.Decline -> {
+ if (isActionConfirmed) {
+ runUpdatingState(asyncAction) {
+ knockRequestsService.declineKnockRequest(currentAction.knockRequest)
+ }
+ } else {
+ asyncAction.value = AsyncAction.ConfirmingNoParams
+ }
+ }
+ is KnockRequestsAction.DeclineAndBan -> {
+ if (isActionConfirmed) {
+ runUpdatingState(asyncAction) {
+ knockRequestsService.declineAndBanKnockRequest(currentAction.knockRequest)
+ }
+ } else {
+ asyncAction.value = AsyncAction.ConfirmingNoParams
+ }
+ }
+ is KnockRequestsAction.AcceptAll -> {
+ if (isActionConfirmed) {
+ runUpdatingState(asyncAction) {
+ knockRequestsService.acceptAllKnockRequests()
+ }
+ } else {
+ asyncAction.value = AsyncAction.ConfirmingNoParams
+ }
+ }
+ KnockRequestsAction.None -> Unit
+ }
+ }
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt
new file mode 100644
index 00000000000..fa33b074a54
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import androidx.compose.runtime.Immutable
+import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions
+import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+import kotlinx.collections.immutable.ImmutableList
+
+data class KnockRequestsListState(
+ val knockRequests: AsyncData>,
+ val currentAction: KnockRequestsAction,
+ val asyncAction: AsyncAction,
+ val permissions: KnockRequestPermissions,
+ val eventSink: (KnockRequestsListEvents) -> Unit,
+) {
+ val canAcceptAll = permissions.canAccept && knockRequests is AsyncData.Success && knockRequests.data.size > 1
+}
+
+@Immutable
+sealed interface KnockRequestsAction {
+ data object None : KnockRequestsAction
+ data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsAction
+ data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsAction
+ data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsAction
+ data object AcceptAll : KnockRequestsAction
+}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
new file mode 100644
index 00000000000..a8d898b08ea
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions
+import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
+import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.core.UserId
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+open class KnockRequestsListStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Loading(),
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf()
+ ),
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable()
+ )
+ ),
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable(
+ reason = "A very long reason that should probably be truncated, " +
+ "but could be also expanded so you can see it over the lines, wow," +
+ "very amazing reason, I know, right, I'm so good at writing reasons."
+ )
+ )
+ ),
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable(),
+ aKnockRequestPresentable(
+ userId = UserId("@user:example.com"),
+ displayName = null,
+ avatarUrl = null,
+ reason = null,
+ )
+ )
+ ),
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable()
+ )
+ ),
+ currentAction = KnockRequestsAction.AcceptAll,
+ asyncAction = AsyncAction.ConfirmingNoParams,
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable()
+ )
+ ),
+ currentAction = KnockRequestsAction.AcceptAll,
+ asyncAction = AsyncAction.Loading,
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable()
+ )
+ ),
+ permissions = KnockRequestPermissions(
+ canAccept = false,
+ canDecline = true,
+ canBan = true,
+ ),
+ currentAction = KnockRequestsAction.AcceptAll,
+ asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")),
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable()
+ )
+ ),
+ permissions = KnockRequestPermissions(
+ canAccept = true,
+ canDecline = false,
+ canBan = true,
+ ),
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable()
+ )
+ ),
+ permissions = KnockRequestPermissions(
+ canAccept = false,
+ canDecline = false,
+ canBan = true,
+ ),
+ ),
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(
+ persistentListOf(
+ aKnockRequestPresentable()
+ )
+ ),
+ permissions = KnockRequestPermissions(
+ canAccept = true,
+ canDecline = true,
+ canBan = false,
+ ),
+ ),
+ )
+}
+
+fun aKnockRequestsListState(
+ knockRequests: AsyncData> = AsyncData.Success(persistentListOf()),
+ currentAction: KnockRequestsAction = KnockRequestsAction.None,
+ asyncAction: AsyncAction = AsyncAction.Uninitialized,
+ permissions: KnockRequestPermissions = KnockRequestPermissions(
+ canAccept = true,
+ canDecline = true,
+ canBan = true,
+ ),
+ eventSink: (KnockRequestsListEvents) -> Unit = {},
+) = KnockRequestsListState(
+ knockRequests = knockRequests,
+ currentAction = currentAction,
+ asyncAction = asyncAction,
+ permissions = permissions,
+ eventSink = eventSink,
+)
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt
new file mode 100644
index 00000000000..09f916ae093
--- /dev/null
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt
@@ -0,0 +1,498 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.knockrequests.impl.R
+import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+import io.element.android.libraries.designsystem.components.BigIcon
+import io.element.android.libraries.designsystem.components.ProgressDialog
+import io.element.android.libraries.designsystem.components.async.AsyncActionView
+import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.text.toDp
+import io.element.android.libraries.designsystem.theme.aliasScreenTitle
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.ButtonSize
+import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.OutlinedButton
+import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.TextButton
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+
+@Composable
+fun KnockRequestsListView(
+ state: KnockRequestsListState,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ KnockRequestsListTopBar(onBackClick = onBackClick)
+ },
+ content = { padding ->
+ KnockRequestsListContent(
+ state = state,
+ modifier = Modifier
+ .padding(padding)
+ .consumeWindowInsets(padding),
+ )
+ }
+ )
+}
+
+@Composable
+private fun KnockRequestsListContent(
+ state: KnockRequestsListState,
+ modifier: Modifier = Modifier,
+) {
+ fun onAcceptClick(knockRequest: KnockRequestPresentable) {
+ state.eventSink(KnockRequestsListEvents.Accept(knockRequest))
+ }
+
+ fun onDeclineClick(knockRequest: KnockRequestPresentable) {
+ state.eventSink(KnockRequestsListEvents.Decline(knockRequest))
+ }
+
+ fun onBanClick(knockRequest: KnockRequestPresentable) {
+ state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequest))
+ }
+
+ var bottomPaddingInPixels by remember { mutableIntStateOf(0) }
+
+ Box(modifier.fillMaxSize()) {
+ when (state.knockRequests) {
+ is AsyncData.Success -> {
+ val knockRequests = state.knockRequests.data
+ if (knockRequests.isEmpty()) {
+ KnockRequestsEmptyList()
+ } else {
+ KnockRequestsList(
+ knockRequests = knockRequests,
+ canAccept = state.permissions.canAccept,
+ canDecline = state.permissions.canDecline,
+ canBan = state.permissions.canBan,
+ onAcceptClick = ::onAcceptClick,
+ onDeclineClick = ::onDeclineClick,
+ onBanClick = ::onBanClick,
+ contentPadding = PaddingValues(bottom = bottomPaddingInPixels.toDp()),
+ )
+ }
+ }
+ is AsyncData.Loading -> {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = spacedBy(16.dp),
+ modifier = Modifier.align(Alignment.Center)
+ ) {
+ CircularProgressIndicator(color = ElementTheme.colors.iconPrimary)
+ Text(
+ text = stringResource(R.string.screen_knock_requests_list_initial_loading_title),
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textPrimary,
+ )
+ }
+ }
+ else -> Unit
+ }
+ KnockRequestsActionsView(
+ currentAction = state.currentAction,
+ asyncAction = state.asyncAction,
+ onConfirm = {
+ state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ },
+ onRetry = {
+ state.eventSink(KnockRequestsListEvents.RetryCurrentAction)
+ },
+ onDismiss = {
+ state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ },
+ )
+ if (state.canAcceptAll) {
+ KnockRequestsAcceptAll(
+ onClick = {
+ state.eventSink(KnockRequestsListEvents.AcceptAll)
+ },
+ onHeightChange = { height ->
+ bottomPaddingInPixels = height
+ },
+ modifier = Modifier.align(Alignment.BottomCenter),
+ )
+ }
+ }
+}
+
+@Composable
+private fun KnockRequestsActionsView(
+ currentAction: KnockRequestsAction,
+ asyncAction: AsyncAction,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit,
+ onRetry: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier) {
+ AsyncActionView(
+ async = asyncAction,
+ onSuccess = { onDismiss() },
+ onErrorDismiss = onDismiss,
+ confirmationDialog = {
+ KnockRequestActionConfirmation(
+ currentAction = currentAction,
+ onSubmit = onConfirm,
+ onDismiss = onDismiss,
+ )
+ },
+ progressDialog = {
+ KnockRequestActionProgress(target = currentAction)
+ },
+ errorMessage = {
+ when (currentAction) {
+ is KnockRequestsAction.Accept -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description)
+ is KnockRequestsAction.Decline -> stringResource(R.string.screen_knock_requests_list_decline_failed_alert_description)
+ is KnockRequestsAction.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_decline_failed_alert_description)
+ KnockRequestsAction.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_all_failed_alert_description)
+ else -> ""
+ }
+ },
+ onRetry = onRetry,
+ )
+ }
+}
+
+@Composable
+private fun KnockRequestActionConfirmation(
+ currentAction: KnockRequestsAction,
+ onSubmit: () -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val (title, content, submitText) = when (currentAction) {
+ KnockRequestsAction.AcceptAll -> Triple(
+ stringResource(R.string.screen_knock_requests_list_accept_all_alert_title),
+ stringResource(R.string.screen_knock_requests_list_accept_all_alert_description),
+ stringResource(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title),
+ )
+ is KnockRequestsAction.Decline -> Triple(
+ stringResource(R.string.screen_knock_requests_list_decline_alert_title),
+ stringResource(R.string.screen_knock_requests_list_decline_alert_description, currentAction.knockRequest.getBestName()),
+ stringResource(R.string.screen_knock_requests_list_decline_alert_confirm_button_title),
+ )
+ is KnockRequestsAction.DeclineAndBan -> Triple(
+ stringResource(R.string.screen_knock_requests_list_ban_alert_title),
+ stringResource(R.string.screen_knock_requests_list_ban_alert_description, currentAction.knockRequest.getBestName()),
+ stringResource(R.string.screen_knock_requests_list_ban_alert_confirm_button_title),
+ )
+ else -> return
+ }
+ ConfirmationDialog(
+ title = title,
+ content = content,
+ submitText = submitText,
+ onSubmitClick = onSubmit,
+ onDismiss = onDismiss,
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun KnockRequestActionProgress(
+ target: KnockRequestsAction,
+ modifier: Modifier = Modifier,
+) {
+ val progressText = when (target) {
+ is KnockRequestsAction.Accept -> stringResource(R.string.screen_knock_requests_list_accept_loading_title)
+ is KnockRequestsAction.Decline -> stringResource(R.string.screen_knock_requests_list_decline_loading_title)
+ is KnockRequestsAction.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_ban_loading_title)
+ KnockRequestsAction.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_all_loading_title)
+ else -> return
+ }
+ ProgressDialog(
+ text = progressText,
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun KnockRequestsList(
+ knockRequests: ImmutableList,
+ canAccept: Boolean,
+ canDecline: Boolean,
+ canBan: Boolean,
+ onAcceptClick: (KnockRequestPresentable) -> Unit,
+ onDeclineClick: (KnockRequestPresentable) -> Unit,
+ onBanClick: (KnockRequestPresentable) -> Unit,
+ modifier: Modifier = Modifier,
+ contentPadding: PaddingValues = PaddingValues(0.dp),
+) {
+ LazyColumn(
+ modifier = modifier.fillMaxSize(),
+ contentPadding = contentPadding,
+ ) {
+ itemsIndexed(knockRequests) { index, knockRequest ->
+ KnockRequestItem(
+ knockRequest = knockRequest,
+ onAcceptClick = onAcceptClick,
+ canBan = canBan,
+ canDecline = canDecline,
+ canAccept = canAccept,
+ onDeclineClick = onDeclineClick,
+ onBanClick = onBanClick,
+ )
+ if (index != knockRequests.size - 1) {
+ HorizontalDivider()
+ }
+ }
+ }
+}
+
+@Composable
+private fun KnockRequestItem(
+ knockRequest: KnockRequestPresentable,
+ canAccept: Boolean,
+ canDecline: Boolean,
+ canBan: Boolean,
+ onAcceptClick: (KnockRequestPresentable) -> Unit,
+ onDeclineClick: (KnockRequestPresentable) -> Unit,
+ onBanClick: (KnockRequestPresentable) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ ) {
+ Avatar(knockRequest.getAvatarData(AvatarSize.KnockRequestItem))
+ Spacer(modifier = Modifier.width(16.dp))
+ Column {
+ // Name and date
+ Row {
+ Text(
+ modifier = Modifier
+ .clipToBounds()
+ .weight(1f),
+ text = knockRequest.getBestName(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.primary,
+ style = ElementTheme.typography.fontBodyLgMedium,
+ )
+ val formattedDate = knockRequest.formattedDate
+ if (!formattedDate.isNullOrEmpty()) {
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = formattedDate,
+ color = MaterialTheme.colorScheme.secondary,
+ style = ElementTheme.typography.fontBodySmRegular,
+ )
+ }
+ }
+ // UserId
+ if (!knockRequest.displayName.isNullOrEmpty()) {
+ Text(
+ text = knockRequest.userId.value,
+ color = MaterialTheme.colorScheme.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ )
+ }
+ // Reason
+ val reason = knockRequest.reason
+ if (!reason.isNullOrBlank()) {
+ Spacer(modifier = Modifier.height(12.dp))
+ var isExpanded by rememberSaveable(knockRequest.userId) { mutableStateOf(false) }
+ var isExpandable by rememberSaveable(knockRequest.userId) { mutableStateOf(false) }
+ Row(
+ verticalAlignment = Alignment.Top,
+ modifier = Modifier
+ .animateContentSize()
+ .clickable(enabled = isExpandable) { isExpanded = !isExpanded }
+ ) {
+ Text(
+ text = reason,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ maxLines = if (isExpanded) Int.MAX_VALUE else 3,
+ onTextLayout = { result ->
+ if (!isExpanded && result.hasVisualOverflow) {
+ isExpandable = true
+ }
+ },
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f),
+ )
+ Box(modifier = Modifier.size(24.dp)) {
+ if (isExpandable) {
+ Icon(
+ imageVector = if (isExpanded) CompoundIcons.ChevronUp() else CompoundIcons.ChevronDown(),
+ contentDescription = null,
+ tint = ElementTheme.colors.iconTertiary,
+ )
+ }
+ }
+ }
+ }
+ // Actions
+ if (canDecline || canAccept) {
+ Spacer(modifier = Modifier.height(12.dp))
+ }
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+ if (canDecline) {
+ OutlinedButton(
+ text = stringResource(CommonStrings.action_decline),
+ onClick = {
+ onDeclineClick(knockRequest)
+ },
+ size = ButtonSize.MediumLowPadding,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ if (canAccept) {
+ Button(
+ text = stringResource(CommonStrings.action_accept),
+ onClick = {
+ onAcceptClick(knockRequest)
+ },
+ size = ButtonSize.MediumLowPadding,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+ if (canBan) {
+ Spacer(modifier = Modifier.height(12.dp))
+ TextButton(
+ text = stringResource(R.string.screen_knock_requests_list_decline_and_ban_action_title),
+ onClick = {
+ onBanClick(knockRequest)
+ },
+ destructive = true,
+ size = ButtonSize.Small,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun KnockRequestsAcceptAll(
+ onClick: () -> Unit,
+ onHeightChange: (Int) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier
+ .shadow(elevation = 24.dp, spotColor = Color.Transparent)
+ .background(color = ElementTheme.colors.bgCanvasDefault)
+ .padding(vertical = 12.dp, horizontal = 16.dp)
+ .onSizeChanged { onHeightChange(it.height) }
+ ) {
+ OutlinedButton(
+ text = stringResource(R.string.screen_knock_requests_list_accept_all_button_title),
+ onClick = onClick,
+ size = ButtonSize.Medium,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+}
+
+@Composable
+private fun KnockRequestsEmptyList(
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier.padding(
+ horizontal = 32.dp,
+ vertical = 48.dp,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ IconTitleSubtitleMolecule(
+ title = stringResource(R.string.screen_knock_requests_list_empty_state_title),
+ subTitle = stringResource(R.string.screen_knock_requests_list_empty_state_description),
+ iconStyle = BigIcon.Style.Default(CompoundIcons.AskToJoin()),
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun KnockRequestsListTopBar(onBackClick: () -> Unit) {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.screen_knock_requests_list_title),
+ style = ElementTheme.typography.aliasScreenTitle,
+ )
+ },
+ navigationIcon = { BackButton(onClick = onBackClick) },
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun KnockRequestsListViewPreview(
+ @PreviewParameter(KnockRequestsListStateProvider::class) state: KnockRequestsListState
+) = ElementPreview {
+ KnockRequestsListView(
+ state = state,
+ onBackClick = {},
+ )
+}
diff --git a/features/knockrequests/impl/src/main/res/values-cs/translations.xml b/features/knockrequests/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 00000000000..c6aa055cd13
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,37 @@
+
+
+ "Ano, přijmout všechny"
+ "Opravdu chcete přijmout všechny žádosti o vstup?"
+ "Přijmout všechny požadavky"
+ "Přijmout vše"
+ "Nemohli jsme přijmout všechny žádosti. Chcete to zkusit znovu?"
+ "Nepodařilo se přijmout všechny žádosti"
+ "Přijímání všech žádostí o vstup"
+ "Tuto žádost jsme nemohli přijmout. Chcete to zkusit znovu?"
+ "Žádost se nepodařilo přijmout"
+ "Přijímání žádosti o vstup"
+ "Ano, odmítnout a vykázat"
+ "Opravdu chcete odmítnout a vykázat %1$s? Tento uživatel nebude moci znovu požádat o vstup do této místnosti."
+ "Odmítnout a zakázat vstup"
+ "Odmítání vstupu a vykázání"
+ "Ano, odmítnout"
+ "Opravdu chcete odmítnout %1$s žádost o vstup do této místnosti?"
+ "Odmítnout vstup"
+ "Odmítnout a vykázat"
+ "Tuto žádost jsme nemohli odmítnout. Chcete to zkusit znovu?"
+ "Žádost se nepodařilo odmítnout"
+ "Odmítání žádosti o vstup"
+ "Když někdo požádá o vstup do místnosti, uvidíte jeho žádost zde."
+ "Žádná čekající žádost o vstup"
+ "Načítání žádostí o vstup…"
+ "Žádosti o vstup"
+
+ - "%1$s +%2$d další chce vstoupit do této místnosti"
+ - "%1$s +%2$d další chtějí vstoupit do této místnosti"
+ - "%1$s +%2$d dalších chce vstoupit do této místnosti"
+
+ "Zobrazit vše"
+ "Přijmout"
+ "%1$s chce vstoupit do této místnosti"
+ "Zobrazit"
+
diff --git a/features/knockrequests/impl/src/main/res/values-de/translations.xml b/features/knockrequests/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 00000000000..70e43ba0765
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,25 @@
+
+
+ "Ja, akzeptiere alle"
+ "Sind Sie sicher, dass Sie alle Beitrittsanfragen akzeptieren möchten?"
+ "Akzeptiere alle Anfragen"
+ "Alle akzeptieren"
+ "Ja, ablehnen und sperren"
+ "Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten ? Dieser Benutzer kann keinen erneuten Zugriff auf diesen Raum anfordern."
+ "Ablehnen und Zugriff verbieten"
+ "Ja, ablehnen"
+ "Sind Sie sicher, dass Sie die %1$s Anfrage, diesem Chatroom beizutreten, ablehnen möchten ?"
+ "Zugriff verweigern"
+ "Ablehnen und sperren"
+ "Falls jemand um Aufnahme in den Raum bittet, können Sie dessen Anfrage hier sehen."
+ "Keine ausstehende Beitrittsanfrage"
+ "Beitrittsanfragen"
+
+ - "%1$s+ %2$d andere wollen diesem Chatroom beitreten"
+ - "%1$s+ %2$d andere wollen diesem Chatroom beitreten"
+
+ "Alles ansehen"
+ "Akzeptieren"
+ "%1$s möchte diesem Chatroom beitreten"
+ "Ansicht"
+
diff --git a/features/knockrequests/impl/src/main/res/values-el/translations.xml b/features/knockrequests/impl/src/main/res/values-el/translations.xml
new file mode 100644
index 00000000000..326227df48f
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-el/translations.xml
@@ -0,0 +1,29 @@
+
+
+ "Ναι, αποδοχή όλων"
+ "Σίγουρα θες να αποδεχτείς όλα τα αιτήματα συμμετοχής;"
+ "Αποδοχή όλων των αιτημάτων"
+ "Αποδοχή όλων"
+ "Αποδοχή όλων των αιτημάτων συμμετοχής"
+ "Γίνεται αποδοχή αιτήματος συμμετοχής"
+ "Ναι, απόρριψη και αποκλεισμός"
+ "Σίγουρα θες να απορρίψειε και να αποκλείσεις τον χρήστη %1$s; Αυτός ο χρήστης δεν θα μπορεί να ζητήσει πρόσβαση για να συμμετάσχει ξανά σε αυτό το δωμάτιο."
+ "Απόρριψη και αποκλεισμός πρόσβασης"
+ "Γίνεται απόρριψη και αποκλεισμός πρόσβασης"
+ "Ναι, απόρριψη"
+ "Σίγουρα θες να απορρίψεις το αίτημα του χρήστη %1$s να συμμετάσχει στο δωμάτιο;"
+ "Απόρριψη πρόσβασης"
+ "Απόρριψη και αποκλεισμός"
+ "Γίνεται απόρριψη αιτήματος συμμετοχής"
+ "Όταν κάποιος θα ζητήσει να συμμετάσχει στο δωμάτιο, θα μπορείς να δεις το αίτημά του εδώ."
+ "Δεν υπάρχει εκκρεμές αίτημα συμμετοχής"
+ "Αιτήματα συμμετοχής"
+
+ - "Οι χρήστες %1$s +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο"
+ - "Οι χρήστες %1$s +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο"
+
+ "Προβολή όλων"
+ "Αποδοχή"
+ "Ο χρήστης %1$s θέλει να μπει σε αυτό το δωμάτιο"
+ "Προβολή"
+
diff --git a/features/knockrequests/impl/src/main/res/values-et/translations.xml b/features/knockrequests/impl/src/main/res/values-et/translations.xml
new file mode 100644
index 00000000000..79d9c56964c
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-et/translations.xml
@@ -0,0 +1,25 @@
+
+
+ "Jah, võta kõik vastu"
+ "Kas sa oled kindel, et soovid kõik vastu liitumist soovinud võtta?"
+ "Võta kõik vastu"
+ "Nõustu kõigiga"
+ "Jah, keeldu liitumisest ning keela ligipääs"
+ "Kas sa oled kindel, et soovid kasutajale %1$s keelata ligipääsu siia jututuppa ning seada talle suhtluskeelu? Seetõttu ta ei saa ka enam hiljem liitumispalvet saata."
+ "Keeldu liitumisest ja keela ligipääs"
+ "Jah, keeldu"
+ "Kas sa oled kindel, et soovid kasutajale %1$s keelata ligipääsu siia jututuppa?"
+ "Keela ligipääs"
+ "Keeldu ja määra suhtluskeeld"
+ "Kui keegi soovib jututoaga liituda, siis need päringud on kuvatud siin."
+ "Pole ühtegi liitumispalvet"
+ "Liitumispalved"
+
+ - "%1$s + veel %2$d kasutaja soovivad selle jututoaga liituda"
+ - "%1$s + veel %2$d kasutajat soovivad selle jututoaga liituda"
+
+ "Vaata kõiki"
+ "Nõustu"
+ "%1$s soovib selle jututoaga liituda"
+ "Vaata"
+
diff --git a/features/knockrequests/impl/src/main/res/values-fi/translations.xml b/features/knockrequests/impl/src/main/res/values-fi/translations.xml
new file mode 100644
index 00000000000..08b8509d31c
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-fi/translations.xml
@@ -0,0 +1,25 @@
+
+
+ "Kyllä, hyväksy kaikki"
+ "Haluatko varmasti hyväksyä kaikki liittymispyynnöt?"
+ "Hyväksy kaikki pyynnöt"
+ "Hyväksy kaikki"
+ "Kyllä, hylkää ja anna porttikielto"
+ "Haluatko varmasti hylätä käyttäjän %1$s pyynnön liittyä huoneeseen ja antaa hänelle porttikiellon? Hän ei voi enää pyytää lupaa liittyä tähän huoneeseen."
+ "Hylkää ja anna porttikielto"
+ "Kyllä, hylkää"
+ "Haluatko varmasti hylätä käyttäjän %1$s pyynnön liittyä tähän huoneeseen?"
+ "Hylkää pyyntö"
+ "Hylkää ja anna porttikielto"
+ "Kun joku pyytää liittyä huoneeseen, näet hänen pyyntönsä täällä."
+ "Ei odottavia liittymispyyntöjä"
+ "Liittymispyynnöt"
+
+ - "%1$s +%2$d muu haluavat liittyä tähän huoneeseen"
+ - "%1$s +%2$d muuta haluavat liittyä tähän huoneeseen"
+
+ "Näytä kaikki"
+ "Hyväksy"
+ "%1$s haluaa liittyä tähän huoneeseen"
+ "Näytä"
+
diff --git a/features/knockrequests/impl/src/main/res/values-fr/translations.xml b/features/knockrequests/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 00000000000..85da9d9f501
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,36 @@
+
+
+ "Oui, tout accepter"
+ "Êtes-vous sûr de vouloir accepter toutes les demandes pour rejoindre le salon ?"
+ "Tout accepter"
+ "Tout accepter"
+ "Toutes les demandes n’ont pas pu être acceptées. Voulez-vous réessayer ?"
+ "Toutes les demandes n’ont pas été acceptées"
+ "Accepter toutes les demandes à rejoindre"
+ "La demande n’a pas pu être acceptée. Voulez-vous réessayer ?"
+ "Impossible d’accepter la demande"
+ "Accepter la demande à rejoindre"
+ "Oui, rejeter et bannir"
+ "Êtes-vous sûr de vouloir rejeter la demande et bannir %1$s ? Cet utilisateur ne pourra pas demander à nouveau à rejoindre ce salon."
+ "Refuser et interdire l’accès"
+ "En cours de traitement…"
+ "Oui, refuser"
+ "Êtes-vous sûr de vouloir refuser la demande de %1$s à rejoindre le salon ?"
+ "Refuser l’accès"
+ "Refuser et bannir"
+ "Nous n’avons pas pu refuser cette demande. Voulez-vous réessayer ?"
+ "Echec"
+ "Traitement en cours…"
+ "Lorsque quelqu’un demandera à rejoindre le salon, vous pourrez voir sa demande ici."
+ "Personne ne demande à rejoindre le salon"
+ "Chargement…"
+ "Demandes en attente"
+
+ - "%1$s et %2$d autre personne souhaitent rejoindre ce salon"
+ - "%1$s et %2$d autres personnes souhaitent rejoindre ce salon"
+
+ "Tout afficher"
+ "Accepter"
+ "%1$s souhaite rejoindre ce salon"
+ "Voir"
+
diff --git a/features/knockrequests/impl/src/main/res/values-hu/translations.xml b/features/knockrequests/impl/src/main/res/values-hu/translations.xml
new file mode 100644
index 00000000000..2ae4a79857d
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-hu/translations.xml
@@ -0,0 +1,36 @@
+
+
+ "Igen, az összes elfogadása"
+ "Biztos, hogy elfogadja az összes csatlakozási kérelmet?"
+ "Minden kérés elfogadása"
+ "Összes elfogadása"
+ "Nem sikerült az összes kérés fogadása. Újra megpróbálja?"
+ "Nem sikerült az összes kérés elfogadása"
+ "Összes csatlakozási kérés elfogadása"
+ "Nem sikerült elfogadni a kérést. Megpróbálja újra?"
+ "Nem sikerült elfogadni a kérést"
+ "Csatlakozási kérés elfogadása"
+ "Igen, elutasítás és kitiltás"
+ "Biztos, hogy elutasítja %1$s kérését és ki is tiltja? Többé nem fogja tudni azt kérni, hogy csatlakozhasson ehhez a szobához."
+ "A hozzáférés elutasítása és kitiltás"
+ "A hozzáférés megtagadása és kitiltás"
+ "Igen, elutasítás"
+ "Biztos, hogy elutasítja %1$s kérését, hogy csatlakozzon a szobához?"
+ "Hozzáférés elutasítása"
+ "Elutasítás és kitiltás"
+ "Nem sikerült elutasítani a kérést. Megpróbálja újra?"
+ "Nem sikerült elutasítani a kérést"
+ "Csatlakozási kérés elutasítása"
+ "Ha valaki csatlakozni kíván a szobához, itt láthatja a kérését."
+ "Nincs függőben lévő csatlakozási kérelem"
+ "Csatlakozási kérések betöltése…"
+ "Csatlakozási kérelmek"
+
+ - "%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"
+ - "%1$s és még %2$d felhasználó szeretne csatlakozni ehhez a szobához"
+
+ "Összes megtekintése"
+ "Elfogadás"
+ "%1$s szeretne csatlakozni ehhez a szobához"
+ "Megtekintés"
+
diff --git a/features/knockrequests/impl/src/main/res/values-it/translations.xml b/features/knockrequests/impl/src/main/res/values-it/translations.xml
new file mode 100644
index 00000000000..ebdba8074ab
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-it/translations.xml
@@ -0,0 +1,25 @@
+
+
+ "Sì, accetta tutte"
+ "Sei sicuro di voler accettare tutte le richieste di accesso?"
+ "Accetta tutte le richieste"
+ "Accetta tutte"
+ "Sì, rifiuta e blocca"
+ "Sei sicuro di voler rifiutare e bloccare %1$s? Questo utente non potrà richiedere nuovamente l\'accesso per entrare in questa stanza."
+ "Rifiuta e blocca l\'accesso"
+ "Sì, rifiuta"
+ "Sei sicuro di voler rifiutare la richiesta di %1$s ad entrare in a questa stanza?"
+ "Rifiuta l\'accesso"
+ "Rifiuta e blocca"
+ "Quando qualcuno ti chiederà di entrare nella stanza, potrai vedere la sua richiesta qui."
+ "Nessuna richiesta di accesso in sospeso"
+ "Richieste di accesso"
+
+ - "%1$s +%2$d vogliono entrare in questa stanza"
+ - "%1$s +%2$d vogliono entrare in questa stanza"
+
+ "Visualizza tutte"
+ "Accetta"
+ "%1$s vuole entrare in questa stanza"
+ "Visualizza"
+
diff --git a/features/knockrequests/impl/src/main/res/values-ru/translations.xml b/features/knockrequests/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 00000000000..1023af2d284
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,26 @@
+
+
+ "Да, принять все"
+ "Вы действительно хотите принять все заявки на присоединение?"
+ "Принять все запросы"
+ "Принять всё"
+ "Да, отклонить и запретить"
+ "Вы уверен, что хочешь отклонить и запретить %1$s? Этот пользователь больше не сможет запросить доступ к этой комнате."
+ "Отклонить и запретить доступ"
+ "Да, отклонить"
+ "Вы уверены, что хотите отклонить %1$s запрос на присоединение к этой комнате?"
+ "Отклонить доступ"
+ "Отклонить и запретить"
+ "Вы сможете увидеть запрос, когда кто-то попросит присоединиться к комнате."
+ "Нет ожидающих запросов на присоединение"
+ "Запросы на присоединение"
+
+ - "%1$s +%2$d хочет присоединиться к этой комнате"
+ - "%1$s +%2$d хотят присоединиться к этой комнате"
+ - "%1$s +%2$d хотят присоединиться к этой комнате"
+
+ "Показать все"
+ "Принять"
+ "%1$s хочет присоединиться к этой комнате"
+ "Просмотр"
+
diff --git a/features/knockrequests/impl/src/main/res/values-sk/translations.xml b/features/knockrequests/impl/src/main/res/values-sk/translations.xml
new file mode 100644
index 00000000000..1504ef86316
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values-sk/translations.xml
@@ -0,0 +1,17 @@
+
+
+ "Prijať všetky"
+ "Odmietnuť a zakázať"
+ "Keď niekto požiada, aby sa pripojil k miestnosti, jeho žiadosť si môžete pozrieť tu."
+ "Žiadna čakajúca žiadosť o pripojenie"
+ "Žiadosti o pripojenie"
+
+ - "%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"
+ - "%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"
+ - "%1$s +%2$d ďalších chce vstúpiť do tejto miestnosti"
+
+ "Zobraziť všetko"
+ "Prijať"
+ "%1$s chce vstúpiť do tejto miestnosti"
+ "Zobraziť"
+
diff --git a/features/knockrequests/impl/src/main/res/values/localazy.xml b/features/knockrequests/impl/src/main/res/values/localazy.xml
new file mode 100644
index 00000000000..454bb8e1931
--- /dev/null
+++ b/features/knockrequests/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,36 @@
+
+
+ "Yes, accept all"
+ "Are you sure you want to accept all requests to join?"
+ "Accept all requests"
+ "Accept all"
+ "We couldn’t accept all requests. Would you like to try again?"
+ "Failed to accept all requests"
+ "Accepting all requests to join"
+ "We couldn’t accept this request. Would you like to try again?"
+ "Failed to accept request"
+ "Accepting request to join"
+ "Yes, decline and ban"
+ "Are you sure you want to decline and ban %1$s? This user won’t be able to request access to join this room again."
+ "Decline and ban from accessing"
+ "Declining and banning access"
+ "Yes, decline"
+ "Are you sure you want to decline %1$s request to join this room?"
+ "Decline access"
+ "Decline and ban"
+ "We couldn’t decline this request. Would you like to try again?"
+ "Failed to decline request"
+ "Declining request to join"
+ "When somebody will ask to join the room, you’ll be able to see their request here."
+ "No pending request to join"
+ "Loading requests to join…"
+ "Requests to join"
+
+ - "%1$s +%2$d other want to join this room"
+ - "%1$s +%2$d others want to join this room"
+
+ "View all"
+ "Accept"
+ "%1$s wants to join this room"
+ "View"
+
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt
new file mode 100644
index 00000000000..70270618040
--- /dev/null
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.banner
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions
+import io.element.android.features.knockrequests.impl.data.KnockRequestsService
+import io.element.android.libraries.matrix.api.room.knock.KnockRequest
+import io.element.android.libraries.matrix.test.A_USER_ID
+import io.element.android.libraries.matrix.test.A_USER_ID_2
+import io.element.android.libraries.matrix.test.A_USER_ID_3
+import io.element.android.libraries.matrix.test.room.knock.FakeKnockRequest
+import io.element.android.tests.testutils.lambda.assert
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class) class KnockRequestsBannerPresenterTest {
+ @Test
+ fun `present - when feature is disabled then the banner should be hidden`() = runTest {
+ val knockRequests = flowOf(listOf(FakeKnockRequest()))
+ val presenter = createKnockRequestsBannerPresenter(isFeatureEnabled = false, knockRequestsFlow = knockRequests)
+ presenter.test {
+ skipItems(1)
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `present - when empty knock request list then the banner should be hidden`() = runTest {
+ val knockRequests = flowOf(emptyList())
+ val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
+ presenter.test {
+ skipItems(1)
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `present - when no permission to manage knock requests then the banner should be hidden`() = runTest {
+ val presenter = createKnockRequestsBannerPresenter(canAcceptKnockRequests = false)
+ presenter.test {
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `present - when everything is setup to manage knocks with data, then the banner should be visible`() = runTest {
+ val knockRequests = flowOf(
+ listOf(
+ FakeKnockRequest(
+ reason = "A reason",
+ )
+ )
+ )
+ val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isTrue()
+ assertThat(state.knockRequests).hasSize(1)
+ assertThat(state.canAccept).isTrue()
+ assertThat(state.reason).isEqualTo("A reason")
+ }
+ }
+ }
+
+ @Test
+ fun `present - when multiple knock requests, the banner should not have reason nor subtitle`() = runTest {
+ val knockRequests = flowOf(
+ listOf(
+ FakeKnockRequest(
+ displayName = "Alice",
+ ),
+ FakeKnockRequest(
+ displayName = "Bob",
+ ),
+ FakeKnockRequest(
+ displayName = "Charlie",
+ ),
+ )
+ )
+ val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isTrue()
+ assertThat(state.knockRequests).hasSize(3)
+ assertThat(state.reason).isNull()
+ assertThat(state.subtitle).isNull()
+ }
+ }
+ }
+
+ @Test
+ fun `present - when there are some seen knock requests, then the banner should filtered them`() = runTest {
+ val knockRequests = flowOf(
+ listOf(
+ FakeKnockRequest(
+ displayName = "Alice",
+ isSeen = true,
+ userId = A_USER_ID
+ ),
+ FakeKnockRequest(
+ displayName = "Bob",
+ isSeen = true,
+ userId = A_USER_ID_2
+ ),
+ FakeKnockRequest(
+ isSeen = false,
+ displayName = "Charlie",
+ reason = "A reason",
+ userId = A_USER_ID_3
+ ),
+ )
+ )
+ val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isTrue()
+ // Only Charlie should be displayed
+ assertThat(state.knockRequests).hasSize(1)
+ assertThat(state.reason).isEqualTo("A reason")
+ assertThat(state.subtitle).isEqualTo(A_USER_ID_3.value)
+ }
+ }
+ }
+
+ @Test
+ fun `present - given AcceptSingleRequest event with failure, then the banner should hide and reappear and error should appear and disappear`() = runTest {
+ val acceptLambda = lambdaRecorder> { Result.failure(Exception()) }
+ val knockRequest = FakeKnockRequest(
+ displayName = "Alice",
+ reason = "A reason",
+ acceptLambda = acceptLambda
+ )
+ val knockRequests = flowOf(listOf(knockRequest))
+ val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
+ }
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isFalse()
+ assertThat(state.displayAcceptError).isFalse()
+ }
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isFalse()
+ assertThat(state.displayAcceptError).isTrue()
+ }
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isTrue()
+ assertThat(state.displayAcceptError).isTrue()
+ }
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isTrue()
+ assertThat(state.displayAcceptError).isFalse()
+ }
+ assert(acceptLambda).isCalledOnce()
+ }
+ }
+
+ @Test
+ fun `present - given an AcceptSingleRequest event with success, then banner should be dismissed`() = runTest {
+ val acceptLambda = lambdaRecorder> { Result.success(Unit) }
+ val knockRequest = FakeKnockRequest(
+ displayName = "Alice",
+ reason = "A reason",
+ acceptLambda = acceptLambda
+ )
+ val knockRequests = flowOf(listOf(knockRequest))
+ val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.knockRequests).hasSize(1)
+ state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
+ }
+ awaitItem().also { state ->
+ assertThat(state.isVisible).isFalse()
+ }
+ advanceUntilIdle()
+ assert(acceptLambda).isCalledOnce()
+ }
+ }
+
+ @Test
+ fun `present - given a Dismiss event, then knock requests should be marked as seen`() = runTest {
+ val markAsSeenLambda = lambdaRecorder> { Result.success(Unit) }
+ val knockRequests = flowOf(
+ listOf(
+ FakeKnockRequest(markAsSeenLambda = markAsSeenLambda),
+ FakeKnockRequest(markAsSeenLambda = markAsSeenLambda),
+ FakeKnockRequest(markAsSeenLambda = markAsSeenLambda),
+ )
+ )
+ val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests)
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ state.eventSink(KnockRequestsBannerEvents.Dismiss)
+ }
+ advanceUntilIdle()
+ assert(markAsSeenLambda).isCalledExactly(3)
+ }
+ }
+}
+
+private fun TestScope.createKnockRequestsBannerPresenter(
+ knockRequestsFlow: Flow> = flowOf(emptyList()),
+ canAcceptKnockRequests: Boolean = true,
+ isFeatureEnabled: Boolean = true,
+): KnockRequestsBannerPresenter {
+ val knockRequestsService = KnockRequestsService(
+ knockRequestsFlow = knockRequestsFlow,
+ coroutineScope = backgroundScope,
+ isKnockFeatureEnabledFlow = flowOf(isFeatureEnabled),
+ permissionsFlow = flowOf(KnockRequestPermissions(canAcceptKnockRequests, canAcceptKnockRequests, canAcceptKnockRequests)),
+ )
+ return KnockRequestsBannerPresenter(
+ knockRequestsService = knockRequestsService,
+ appCoroutineScope = this,
+ )
+}
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt
new file mode 100644
index 00000000000..ec594086fcc
--- /dev/null
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.banner
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.features.knockrequests.impl.R
+import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EnsureNeverCalled
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.ensureCalledOnce
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class KnockRequestsBannerViewTest {
+ @get:Rule
+ val rule = createAndroidComposeRule()
+
+ @Test
+ fun `clicking on view on single request invoke the expected callback`() {
+ val eventsRecorder = EventsRecorder(expectEvents = false)
+ ensureCalledOnce {
+ rule.setKnockRequestsBannerView(
+ state = aKnockRequestsBannerState(
+ eventSink = eventsRecorder,
+ ),
+ onViewRequestsClick = it
+ )
+ rule.clickOn(R.string.screen_room_single_knock_request_view_button_title)
+ }
+ }
+
+ @Test
+ fun `clicking on view all when multiple requests invoke the expected callback`() {
+ val eventsRecorder = EventsRecorder(expectEvents = false)
+ ensureCalledOnce {
+ rule.setKnockRequestsBannerView(
+ state = aKnockRequestsBannerState(
+ knockRequests = listOf(
+ aKnockRequestPresentable(displayName = "Alice"),
+ aKnockRequestPresentable(displayName = "Bob"),
+ aKnockRequestPresentable(displayName = "Charlie")
+ ),
+ eventSink = eventsRecorder,
+ ),
+ onViewRequestsClick = it
+ )
+ rule.clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title)
+ }
+ }
+
+ @Test
+ fun `clicking on accept on a single request emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setKnockRequestsBannerView(
+ state = aKnockRequestsBannerState(
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_accept)
+ eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest)
+ }
+
+ @Test
+ fun `clicking on dismiss emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ rule.setKnockRequestsBannerView(
+ state = aKnockRequestsBannerState(
+ eventSink = eventsRecorder,
+ ),
+ )
+ val close = rule.activity.getString(CommonStrings.action_close)
+ rule.onNodeWithContentDescription(close).performClick()
+ eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss)
+ }
+}
+
+private fun AndroidComposeTestRule.setKnockRequestsBannerView(
+ state: KnockRequestsBannerState,
+ onViewRequestsClick: () -> Unit = EnsureNeverCalled(),
+) {
+ setContent {
+ KnockRequestsBannerView(
+ state = state,
+ onViewRequestsClick = onViewRequestsClick,
+ )
+ }
+}
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt
new file mode 100644
index 00000000000..d74155ead15
--- /dev/null
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package io.element.android.features.knockrequests.impl.list
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions
+import io.element.android.features.knockrequests.impl.data.KnockRequestsService
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.room.knock.KnockRequest
+import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
+import io.element.android.libraries.matrix.test.room.knock.FakeKnockRequest
+import io.element.android.tests.testutils.lambda.assert
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class KnockRequestsListPresenterTest {
+ @Test
+ fun `present - initial states should be emitted`() = runTest {
+ val presenter = createKnockRequestsListPresenter()
+ presenter.test {
+ awaitItem().also { state ->
+ assertThat(state.knockRequests).isInstanceOf(AsyncData.Loading::class.java)
+ assertThat(state.permissions.canAccept).isFalse()
+ assertThat(state.permissions.canDecline).isFalse()
+ assertThat(state.permissions.canBan).isFalse()
+ }
+ awaitItem().also { state ->
+ assertThat(state.knockRequests).isInstanceOf(AsyncData.Loading::class.java)
+ assertThat(state.permissions.canAccept).isTrue()
+ assertThat(state.permissions.canDecline).isTrue()
+ assertThat(state.permissions.canBan).isTrue()
+ }
+ awaitItem().also { state ->
+ assertThat(state.knockRequests).isInstanceOf(AsyncData.Success::class.java)
+ assertThat(state.knockRequests.dataOrNull()).isEmpty()
+ }
+ }
+ }
+
+ @Test
+ fun `present - accept success scenario`() = runTest {
+ val acceptLambda = lambdaRecorder> { Result.success(Unit) }
+ val knockRequest = FakeKnockRequest(acceptLambda = acceptLambda)
+ val knockRequests = flowOf(listOf(knockRequest))
+ val presenter = createKnockRequestsListPresenter(
+ knockRequestsFlow = knockRequests
+ )
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
+ state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable))
+ }
+ skipItems(1)
+ awaitItem().also { state ->
+ val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.Accept(knockRequestPresentable))
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
+ state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ }
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None)
+ assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty()
+ }
+ assert(acceptLambda).isCalledOnce()
+ }
+ }
+
+ @Test
+ fun `present - accept failure scenario`() = runTest {
+ val acceptLambda = lambdaRecorder> { Result.failure(Exception()) }
+ val knockRequest = FakeKnockRequest(acceptLambda = acceptLambda)
+ val knockRequests = flowOf(listOf(knockRequest))
+ val presenter = createKnockRequestsListPresenter(
+ knockRequestsFlow = knockRequests
+ )
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
+ state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable))
+ }
+ skipItems(1)
+ awaitItem().also { state ->
+ val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.Accept(knockRequestPresentable))
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
+ state.eventSink(KnockRequestsListEvents.RetryCurrentAction)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
+ state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ }
+ skipItems(1)
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None)
+ assertThat(state.knockRequests.dataOrNull()).hasSize(1)
+ }
+ assert(acceptLambda).isCalledExactly(2)
+ }
+ }
+
+ @Test
+ fun `present - decline success scenario`() = runTest {
+ val declineLambda = lambdaRecorder> { Result.success(Unit) }
+ val knockRequest = FakeKnockRequest(declineLambda = declineLambda)
+ val knockRequests = flowOf(listOf(knockRequest))
+ val presenter = createKnockRequestsListPresenter(
+ knockRequestsFlow = knockRequests
+ )
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
+ state.eventSink(KnockRequestsListEvents.Decline(knockRequestPresentable))
+ }
+ skipItems(1)
+ awaitItem().also { state ->
+ val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.Decline(knockRequestPresentable))
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
+ state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
+ state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ }
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None)
+ assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty()
+ }
+ }
+ assert(declineLambda).isCalledOnce()
+ }
+
+ @Test
+ fun `present - decline and ban success scenario`() = runTest {
+ val declineAndBanLambda = lambdaRecorder> { Result.success(Unit) }
+ val knockRequest = FakeKnockRequest(declineAndBanLambda = declineAndBanLambda)
+ val knockRequests = flowOf(listOf(knockRequest))
+ val presenter = createKnockRequestsListPresenter(
+ knockRequestsFlow = knockRequests
+ )
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
+ state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequestPresentable))
+ }
+ skipItems(1)
+ awaitItem().also { state ->
+ val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.DeclineAndBan(knockRequestPresentable))
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
+ state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
+ state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ }
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None)
+ assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty()
+ }
+ }
+ assert(declineAndBanLambda).isCalledOnce()
+ }
+
+ @Test
+ fun `present - accept all success scenario`() = runTest {
+ val acceptLambda = lambdaRecorder> { Result.success(Unit) }
+ val knockRequests = flowOf(
+ listOf(
+ FakeKnockRequest(eventId = AN_EVENT_ID, acceptLambda = acceptLambda),
+ FakeKnockRequest(eventId = AN_EVENT_ID_2, acceptLambda = acceptLambda),
+ )
+ )
+ val presenter = createKnockRequestsListPresenter(
+ knockRequestsFlow = knockRequests
+ )
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.canAcceptAll).isTrue()
+ state.eventSink(KnockRequestsListEvents.AcceptAll)
+ }
+ skipItems(1)
+ awaitItem().also { state ->
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.AcceptAll)
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
+ state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
+ state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ }
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None)
+ assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty()
+ }
+ }
+ assert(acceptLambda).isCalledExactly(2)
+ }
+
+ @Test
+ fun `present - accept all partial success scenario`() = runTest {
+ val acceptSuccessLambda = lambdaRecorder> { Result.success(Unit) }
+ val acceptFailureLambda = lambdaRecorder> { Result.failure(Exception()) }
+ val knockRequests = flowOf(
+ listOf(
+ FakeKnockRequest(eventId = AN_EVENT_ID, acceptLambda = acceptSuccessLambda),
+ FakeKnockRequest(eventId = AN_EVENT_ID_2, acceptLambda = acceptFailureLambda),
+ )
+ )
+ val presenter = createKnockRequestsListPresenter(
+ knockRequestsFlow = knockRequests
+ )
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.canAcceptAll).isTrue()
+ state.eventSink(KnockRequestsListEvents.AcceptAll)
+ }
+ skipItems(1)
+ awaitItem().also { state ->
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.AcceptAll)
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
+ state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
+ state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ }
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
+ assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None)
+ assertThat(state.knockRequests.dataOrNull()).hasSize(1)
+ }
+ }
+ assert(acceptFailureLambda).isCalledOnce()
+ assert(acceptSuccessLambda).isCalledOnce()
+ }
+
+ private fun TestScope.createKnockRequestsListPresenter(
+ canAccept: Boolean = true,
+ canDecline: Boolean = true,
+ canBan: Boolean = true,
+ knockRequestsFlow: Flow> = flowOf(emptyList())
+ ): KnockRequestsListPresenter {
+ val knockRequestsService = KnockRequestsService(
+ knockRequestsFlow = knockRequestsFlow,
+ coroutineScope = backgroundScope,
+ isKnockFeatureEnabledFlow = flowOf(true),
+ permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)),
+ )
+ return KnockRequestsListPresenter(knockRequestsService = knockRequestsService)
+ }
+}
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt
new file mode 100644
index 00000000000..af2bfefd164
--- /dev/null
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.knockrequests.impl.list
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.features.knockrequests.impl.R
+import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable
+import io.element.android.libraries.architecture.AsyncAction
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EnsureNeverCalled
+import io.element.android.tests.testutils.EventsRecorder
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.ensureCalledOnce
+import io.element.android.tests.testutils.pressBack
+import kotlinx.collections.immutable.persistentListOf
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class KnockRequestsListViewTest {
+ @get:Rule val rule = createAndroidComposeRule()
+
+ @Test
+ fun `clicking on back invoke the expected callback`() {
+ val eventsRecorder = EventsRecorder(expectEvents = false)
+ ensureCalledOnce {
+ rule.setKnockRequestsListView(
+ aKnockRequestsListState(
+ eventSink = eventsRecorder,
+ ),
+ onBackClick = it
+ )
+ rule.pressBack()
+ }
+ }
+
+ @Test
+ fun `clicking on accept emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val knockRequest = aKnockRequestPresentable()
+ rule.setKnockRequestsListView(
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_accept)
+ eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest))
+ }
+
+ @Test
+ fun `clicking on decline emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val knockRequest = aKnockRequestPresentable()
+ rule.setKnockRequestsListView(
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_decline)
+ eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest))
+ }
+
+ @Test
+ fun `clicking on decline and ban emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val knockRequest = aKnockRequestPresentable()
+ rule.setKnockRequestsListView(
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(persistentListOf(knockRequest)),
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title)
+ eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest))
+ }
+
+ @Test
+ fun `clicking on accept all emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
+ rule.setKnockRequestsListView(
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(knockRequests),
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(R.string.screen_knock_requests_list_accept_all_button_title)
+ eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll)
+ }
+
+ @Test
+ fun `retry on async view retry emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
+ rule.setKnockRequestsListView(
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(knockRequests),
+ asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")),
+ currentAction = KnockRequestsAction.AcceptAll,
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_retry)
+ eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction)
+ }
+
+ @Test
+ fun `canceling async view emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
+ rule.setKnockRequestsListView(
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(knockRequests),
+ asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")),
+ currentAction = KnockRequestsAction.AcceptAll,
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(CommonStrings.action_cancel)
+ eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction)
+ }
+
+ @Test
+ fun `confirming async view emit the expected event`() {
+ val eventsRecorder = EventsRecorder()
+ val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
+ rule.setKnockRequestsListView(
+ aKnockRequestsListState(
+ knockRequests = AsyncData.Success(knockRequests),
+ asyncAction = AsyncAction.ConfirmingNoParams,
+ currentAction = KnockRequestsAction.AcceptAll,
+ eventSink = eventsRecorder,
+ ),
+ )
+ rule.clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title)
+ eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction)
+ }
+}
+
+private fun AndroidComposeTestRule.setKnockRequestsListView(
+ state: KnockRequestsListState,
+ onBackClick: () -> Unit = EnsureNeverCalled(),
+) {
+ setContent {
+ KnockRequestsListView(
+ state = state,
+ onBackClick = onBackClick,
+ )
+ }
+}
diff --git a/features/leaveroom/api/src/main/res/values-fr/translations.xml b/features/leaveroom/api/src/main/res/values-fr/translations.xml
index bed2bdd3653..2c801ed76a6 100644
--- a/features/leaveroom/api/src/main/res/values-fr/translations.xml
+++ b/features/leaveroom/api/src/main/res/values-fr/translations.xml
@@ -1,6 +1,6 @@
- "Êtes-vous sûr de vouloir quitter cette discussion? Vous ne pourrez pas la rejoindre à nouveau sans y être invité."
+ "Êtes-vous sûr de vouloir quitter cette discussion ? Vous ne pourrez pas la rejoindre à nouveau sans y être invité."
"Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à l’avenir, y compris vous."
"Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation."
"Êtes-vous sûr de vouloir quitter le salon ?"
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListEvent.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListEvent.kt
new file mode 100644
index 00000000000..b1262708fc5
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListEvent.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.licenses.impl.list
+
+sealed interface DependencyLicensesListEvent {
+ data class SetFilter(val filter: String) : DependencyLicensesListEvent
+}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
index 8b01b00afe8..d7ad980dd16 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
@@ -29,6 +29,10 @@ class DependencyLicensesListPresenter @Inject constructor(
var licenses by remember {
mutableStateOf>>(AsyncData.Loading())
}
+ var filteredLicenses by remember {
+ mutableStateOf>>(AsyncData.Loading())
+ }
+ var filter by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
runCatching {
licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
@@ -36,6 +40,32 @@ class DependencyLicensesListPresenter @Inject constructor(
licenses = AsyncData.Failure(it)
}
}
- return DependencyLicensesListState(licenses = licenses)
+ LaunchedEffect(filter, licenses.dataOrNull()) {
+ val data = licenses.dataOrNull()
+ val safeFilter = filter.trim()
+ if (data != null && safeFilter.isNotEmpty()) {
+ filteredLicenses = AsyncData.Success(data.filter {
+ it.safeName.contains(safeFilter, ignoreCase = true) ||
+ it.groupId.contains(safeFilter, ignoreCase = true) ||
+ it.artifactId.contains(safeFilter, ignoreCase = true)
+ }.toPersistentList())
+ } else {
+ filteredLicenses = licenses
+ }
+ }
+
+ fun handleEvent(dependencyLicensesListEvent: DependencyLicensesListEvent) {
+ when (dependencyLicensesListEvent) {
+ is DependencyLicensesListEvent.SetFilter -> {
+ filter = dependencyLicensesListEvent.filter
+ }
+ }
+ }
+
+ return DependencyLicensesListState(
+ licenses = filteredLicenses,
+ filter = filter,
+ eventSink = ::handleEvent,
+ )
}
}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
index c60c49c81be..fd9b1ccf1ca 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
@@ -13,4 +13,6 @@ import kotlinx.collections.immutable.ImmutableList
data class DependencyLicensesListState(
val licenses: AsyncData>,
+ val filter: String,
+ val eventSink: (DependencyLicensesListEvent) -> Unit,
)
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
index dcbae607cb7..74c4e424c0b 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
@@ -11,28 +11,49 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.features.licenses.impl.model.License
import io.element.android.libraries.architecture.AsyncData
+import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
open class DependencyLicensesListStateProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
- DependencyLicensesListState(
+ aDependencyLicensesListState(
licenses = AsyncData.Loading()
),
- DependencyLicensesListState(
+ aDependencyLicensesListState(
licenses = AsyncData.Failure(Exception("Failed to load licenses"))
),
- DependencyLicensesListState(
+ aDependencyLicensesListState(
licenses = AsyncData.Success(
persistentListOf(
aDependencyLicenseItem(),
aDependencyLicenseItem(name = null),
)
)
- )
+ ),
+ aDependencyLicensesListState(
+ licenses = AsyncData.Success(
+ persistentListOf(
+ aDependencyLicenseItem(),
+ aDependencyLicenseItem(name = null),
+ )
+ ),
+ filter = "a filter",
+ ),
)
}
+private fun aDependencyLicensesListState(
+ licenses: AsyncData>,
+ filter: String = "",
+): DependencyLicensesListState {
+ return DependencyLicensesListState(
+ licenses = licenses,
+ filter = filter,
+ eventSink = {},
+ )
+}
+
internal fun aDependencyLicenseItem(
name: String? = "A dependency",
) = DependencyLicenseItem(
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
index 740025ce174..f8ce40f1cda 100644
--- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
@@ -7,31 +7,36 @@
package io.element.android.features.licenses.impl.list
+import androidx.compose.foundation.ExperimentalFoundationApi
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.licenses.impl.model.DependencyLicenseItem
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun DependencyLicensesListView(
state: DependencyLicensesListState,
@@ -48,48 +53,64 @@ fun DependencyLicensesListView(
)
},
) { contentPadding ->
- LazyColumn(
+ Column(
modifier = Modifier
.padding(contentPadding)
.padding(horizontal = 16.dp)
) {
- when (state.licenses) {
- is AsyncData.Failure -> item {
- Text(
- text = stringResource(CommonStrings.common_error),
- modifier = Modifier.padding(16.dp)
- )
- }
- AsyncData.Uninitialized,
- is AsyncData.Loading -> item {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 64.dp)
- ) {
- CircularProgressIndicator(
- modifier = Modifier.align(Alignment.Center)
+ if (state.licenses.isSuccess()) {
+ // Search field
+ OutlinedTextField(
+ value = state.filter,
+ onValueChange = { state.eventSink(DependencyLicensesListEvent.SetFilter(it)) },
+ leadingIcon = {
+ Icon(
+ imageVector = CompoundIcons.Search(),
+ contentDescription = null,
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ LazyColumn {
+ when (state.licenses) {
+ is AsyncData.Failure -> item {
+ Text(
+ text = stringResource(CommonStrings.common_error),
+ modifier = Modifier.padding(16.dp)
)
}
- }
- is AsyncData.Success -> items(state.licenses.data) { license ->
- ListItem(
- headlineContent = { Text(license.safeName) },
- supportingContent = {
- Text(
- buildString {
- append(license.groupId)
- append(":")
- append(license.artifactId)
- append(":")
- append(license.version)
- }
+ AsyncData.Uninitialized,
+ is AsyncData.Loading -> item {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 64.dp)
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center)
)
- },
- onClick = {
- onOpenLicense(license)
}
- )
+ }
+ is AsyncData.Success -> items(state.licenses.data) { license ->
+ ListItem(
+ headlineContent = { Text(license.safeName) },
+ supportingContent = {
+ Text(
+ buildString {
+ append(license.groupId)
+ append(":")
+ append(license.artifactId)
+ append(":")
+ append(license.version)
+ }
+ )
+ },
+ onClick = {
+ onOpenLicense(license)
+ }
+ )
+ }
}
}
}
diff --git a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
index 26c4a1ce6f1..46dfa330818 100644
--- a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
+++ b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
@@ -33,6 +33,7 @@ class DependencyLicensesListPresenterTest {
val finalState = awaitItem()
assertThat(finalState.licenses.isSuccess()).isTrue()
assertThat(finalState.licenses.dataOrNull()).isEmpty()
+ assertThat(finalState.filter).isEqualTo("")
}
}
@@ -54,6 +55,40 @@ class DependencyLicensesListPresenterTest {
}
}
+ @Test
+ fun `present - initial state, one license, set filter`() = runTest {
+ val anItem = aDependencyLicenseItem()
+ val presenter = createPresenter {
+ listOf(anItem)
+ }
+ moleculeFlow(RecompositionMode.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
+ val loadedState = awaitItem()
+ assertThat(loadedState.licenses.isSuccess()).isTrue()
+ assertThat(loadedState.licenses.dataOrNull()!!.size).isEqualTo(1)
+ loadedState.eventSink(DependencyLicensesListEvent.SetFilter("dep"))
+ awaitItem().let { state ->
+ assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
+ assertThat(state.filter).isEqualTo("dep")
+ }
+ loadedState.eventSink(DependencyLicensesListEvent.SetFilter("bleh"))
+ skipItems(1)
+ awaitItem().let { state ->
+ assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(0)
+ assertThat(state.filter).isEqualTo("bleh")
+ }
+ loadedState.eventSink(DependencyLicensesListEvent.SetFilter(""))
+ skipItems(1)
+ awaitItem().let { state ->
+ assertThat(state.licenses.dataOrNull()!!.size).isEqualTo(1)
+ assertThat(state.filter).isEqualTo("")
+ }
+ }
+ }
+
private fun createPresenter(
provideResult: () -> List
) = DependencyLicensesListPresenter(
diff --git a/features/lockscreen/impl/src/main/res/values-cs/translations.xml b/features/lockscreen/impl/src/main/res/values-cs/translations.xml
index b9984b1dc3e..fce1142f2c0 100644
--- a/features/lockscreen/impl/src/main/res/values-cs/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-cs/translations.xml
@@ -3,6 +3,7 @@
"Biometrické ověřování"
"biometrické odemknutí"
"Odemkněte pomocí biometrie"
+ "Potvrďte biometrické údaje"
"Zapomněli jste PIN?"
"Změnit PIN kód"
"Povolit biometrické odemykání"
diff --git a/features/lockscreen/impl/src/main/res/values-fi/translations.xml b/features/lockscreen/impl/src/main/res/values-fi/translations.xml
index 6ed4ea81bed..ae2abef6e83 100644
--- a/features/lockscreen/impl/src/main/res/values-fi/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-fi/translations.xml
@@ -3,6 +3,7 @@
"biometrinen tunnistus"
"biometrinen tunnistus"
"Avaa biometrisellä"
+ "Vahvista biometrinen tunniste"
"Unohtuiko PIN-koodi?"
"Vaihda PIN-koodi"
"Salli biometrinen tunnistus"
diff --git a/features/lockscreen/impl/src/main/res/values-fr/translations.xml b/features/lockscreen/impl/src/main/res/values-fr/translations.xml
index b77df23254c..8596647aac8 100644
--- a/features/lockscreen/impl/src/main/res/values-fr/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-fr/translations.xml
@@ -4,12 +4,12 @@
"déverrouillage biométrique"
"Déverrouiller avec la biométrie"
"Confirmer la biométrie"
- "Code PIN oublié?"
+ "Code PIN oublié ?"
"Modifier le code PIN"
"Autoriser le déverrouillage biométrique"
"Supprimer le code PIN"
- "Êtes-vous certain de vouloir supprimer le code PIN?"
- "Supprimer le code PIN?"
+ "Êtes-vous certain de vouloir supprimer le code PIN ?"
+ "Supprimer le code PIN ?"
"Autoriser %1$s"
"Je préfère utiliser le code PIN"
"Gagnez du temps en utilisant %1$s pour déverrouiller l’application à chaque fois."
diff --git a/features/lockscreen/impl/src/main/res/values-hu/translations.xml b/features/lockscreen/impl/src/main/res/values-hu/translations.xml
index 2df43b3fae1..3a652396975 100644
--- a/features/lockscreen/impl/src/main/res/values-hu/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-hu/translations.xml
@@ -3,6 +3,7 @@
"biometrikus hitelesítés"
"biometrikus feloldás"
"Feloldás biometrikus adatokkal"
+ "Biometrikus megerősítés"
"Elfelejtette a PIN-kódot?"
"PIN-kód módosítása"
"Biometrikus feloldás engedélyezése"
diff --git a/features/lockscreen/impl/src/main/res/values-it/translations.xml b/features/lockscreen/impl/src/main/res/values-it/translations.xml
index cdb48f8f63a..5f6fa68ff67 100644
--- a/features/lockscreen/impl/src/main/res/values-it/translations.xml
+++ b/features/lockscreen/impl/src/main/res/values-it/translations.xml
@@ -3,6 +3,7 @@
"autenticazione biometrica"
"sblocco con biometria"
"Sblocca con biometria"
+ "Conferma la biometria"
"PIN dimenticato?"
"Modifica il codice PIN"
"Consenti lo sblocco biometrico"
diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml
index 2b8071fd0b2..5b82829b80b 100644
--- a/features/login/impl/src/main/res/values-it/translations.xml
+++ b/features/login/impl/src/main/res/values-it/translations.xml
@@ -60,6 +60,7 @@ Prova ad accedere manualmente o scansiona il codice QR con un altro dispositivo.
"Seleziona %1$s"
"\"Collega un nuovo dispositivo\""
"Scansiona il codice QR con questo dispositivo"
+ "Disponibile solo se il provider del tuo account lo supporta."
"Apri %1$s su un altro dispositivo per ottenere il codice QR"
"Usa il codice QR mostrato sull\'altro dispositivo."
"Riprova"
diff --git a/features/logout/impl/src/main/res/values-fr/translations.xml b/features/logout/impl/src/main/res/values-fr/translations.xml
index c21194989f8..5e8f9d468d9 100644
--- a/features/logout/impl/src/main/res/values-fr/translations.xml
+++ b/features/logout/impl/src/main/res/values-fr/translations.xml
@@ -14,5 +14,5 @@
"Vous êtes sur le point de vous déconnecter de votre dernier appareil. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos messages."
"La récupération n’est pas configurée."
"Vous êtes sur le point de vous déconnecter de votre dernière session. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."
- "Avez-vous sauvegardé votre clé de récupération?"
+ "Avez-vous sauvegardé votre clé de récupération ?"
diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts
index 824e8c1692d..f5b65209963 100644
--- a/features/messages/impl/build.gradle.kts
+++ b/features/messages/impl/build.gradle.kts
@@ -47,6 +47,7 @@ dependencies {
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.roomselect.api)
+ implementation(projects.libraries.voiceplayer.api)
implementation(projects.libraries.voicerecorder.api)
implementation(projects.libraries.mediaplayer.api)
implementation(projects.libraries.uiUtils)
@@ -65,6 +66,7 @@ dependencies {
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.zoomableimage)
implementation(libs.matrix.emojibase.bindings)
+ implementation(projects.features.knockrequests.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
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 8cbb7b6d748..1b9c9909f45 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
@@ -26,6 +26,7 @@ import im.vector.app.features.analytics.plan.Interaction
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint
+import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.SendLocationEntryPoint
import io.element.android.features.location.api.ShowLocationEntryPoint
@@ -46,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
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.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackWithOverlayBox
@@ -54,6 +56,8 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
@@ -95,6 +99,8 @@ class MessagesFlowNode @AssistedInject constructor(
private val mentionSpanTheme: MentionSpanTheme,
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
private val timelineController: TimelineController,
+ private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
+ private val dateFormatter: DateFormatter,
) : BaseFlowNode(
backstack = BackStack(
initialElement = plugins.filterIsInstance().first().initialTarget.toNavTarget(),
@@ -115,6 +121,7 @@ class MessagesFlowNode @AssistedInject constructor(
@Parcelize
data class MediaViewer(
+ val eventId: EventId?,
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
@@ -146,6 +153,9 @@ class MessagesFlowNode @AssistedInject constructor(
@Parcelize
data object PinnedMessagesList : NavTarget
+
+ @Parcelize
+ data object KnockRequestsList : NavTarget
}
private val callbacks = plugins()
@@ -226,22 +236,30 @@ class MessagesFlowNode @AssistedInject constructor(
override fun onViewAllPinnedEvents() {
backstack.push(NavTarget.PinnedMessagesList)
}
+
+ override fun onViewKnockRequests() {
+ backstack.push(NavTarget.KnockRequestsList)
+ }
}
val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId)
createNode(buildContext, listOf(callback, inputs))
}
is NavTarget.MediaViewer -> {
val params = MediaViewerEntryPoint.Params(
+ eventId = navTarget.eventId,
mediaInfo = navTarget.mediaInfo,
mediaSource = navTarget.mediaSource,
thumbnailSource = navTarget.thumbnailSource,
- canDownload = true,
- canShare = true,
+ canShowInfo = true,
)
val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() {
overlay.hide()
}
+
+ override fun onViewInTimeline(eventId: EventId) {
+ viewInTimeline(eventId)
+ }
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
.params(params)
@@ -302,11 +320,7 @@ class MessagesFlowNode @AssistedInject constructor(
}
override fun onViewInTimelineClick(eventId: EventId) {
- val permalinkData = PermalinkData.RoomLink(
- roomIdOrAlias = room.roomId.toRoomIdOrAlias(),
- eventId = eventId,
- )
- callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) }
+ viewInTimeline(eventId)
}
override fun onRoomPermalinkClick(data: PermalinkData.RoomLink) {
@@ -326,9 +340,20 @@ class MessagesFlowNode @AssistedInject constructor(
NavTarget.Empty -> {
node(buildContext) {}
}
+ NavTarget.KnockRequestsList -> {
+ knockRequestsListEntryPoint.createNode(this, buildContext)
+ }
}
}
+ private fun viewInTimeline(eventId: EventId) {
+ val permalinkData = PermalinkData.RoomLink(
+ roomIdOrAlias = room.roomId.toRoomIdOrAlias(),
+ eventId = eventId,
+ )
+ callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) }
+ }
+
private fun processEventClick(event: TimelineItem.Event): Boolean {
val navTarget = when (event.content) {
is TimelineItemImageContent -> {
@@ -403,14 +428,25 @@ class MessagesFlowNode @AssistedInject constructor(
thumbnailSource: MediaSource?,
): NavTarget {
return NavTarget.MediaViewer(
+ eventId = event.eventId,
mediaInfo = MediaInfo(
filename = content.filename,
caption = content.caption,
mimeType = content.mimeType,
formattedFileSize = content.formattedFileSize,
fileExtension = content.fileExtension,
+ senderId = event.senderId,
senderName = event.safeSenderName,
- dateSent = event.sentTime,
+ senderAvatar = event.senderAvatar.url,
+ dateSent = dateFormatter.format(
+ event.sentTimeMillis,
+ mode = DateFormatterMode.Day,
+ ),
+ dateSentFull = dateFormatter.format(
+ timestamp = event.sentTimeMillis,
+ mode = DateFormatterMode.Full,
+ ),
+ waveform = (content as? TimelineItemVoiceContent)?.waveform,
),
mediaSource = mediaSource,
thumbnailSource = thumbnailSource,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
index 7d5bad4d639..4ee44fbc809 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
@@ -28,6 +28,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
+import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
import io.element.android.features.messages.impl.attachments.Attachment
@@ -71,6 +72,7 @@ class MessagesNode @AssistedInject constructor(
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val mediaPlayer: MediaPlayer,
private val permalinkParser: PermalinkParser,
+ private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer
) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val presenter = presenterFactory.create(
navigator = this,
@@ -98,6 +100,7 @@ class MessagesNode @AssistedInject constructor(
fun onEditPollClick(eventId: EventId)
fun onJoinCallClick(roomId: RoomId)
fun onViewAllPinnedEvents()
+ fun onViewKnockRequests()
}
override fun onBuilt() {
@@ -206,6 +209,10 @@ class MessagesNode @AssistedInject constructor(
callbacks.forEach { it.onJoinCallClick(room.roomId) }
}
+ private fun onViewKnockRequestsClick() {
+ callbacks.forEach { it.onViewKnockRequests() }
+ }
+
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
@@ -231,6 +238,12 @@ class MessagesNode @AssistedInject constructor(
onCreatePollClick = this::onCreatePollClick,
onJoinCallClick = this::onJoinCallClick,
onViewAllPinnedMessagesClick = this::onViewAllPinnedMessagesClick,
+ knockRequestsBannerView = {
+ knockRequestsBannerRenderer.View(
+ modifier = Modifier,
+ onViewRequestsClick = this::onViewKnockRequestsClick
+ )
+ },
modifier = modifier,
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
index 2b9f13be597..c490e8f6df8 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
@@ -275,7 +275,8 @@ class MessagesPresenter @AssistedInject constructor(
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
- TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
+ TimelineItemAction.Edit,
+ TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState)
TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent)
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 78beeedb042..10257bb702b 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
@@ -118,6 +118,7 @@ fun MessagesView(
onViewAllPinnedMessagesClick: () -> Unit,
modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false,
+ knockRequestsBannerView: @Composable () -> Unit,
) {
OnLifecycleEvent { _, event ->
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
@@ -195,8 +196,8 @@ fun MessagesView(
MessagesViewContent(
state = state,
modifier = Modifier
- .padding(padding)
- .consumeWindowInsets(padding),
+ .padding(padding)
+ .consumeWindowInsets(padding),
onContentClick = ::onContentClick,
onMessageLongClick = ::onMessageLongClick,
onUserDataClick = { hidingKeyboard { onUserDataClick(it) } },
@@ -215,6 +216,7 @@ fun MessagesView(
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
onJoinCallClick = onJoinCallClick,
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
+ knockRequestsBannerView = knockRequestsBannerView,
)
},
snackbarHost = {
@@ -284,12 +286,13 @@ private fun MessagesViewContent(
forceJumpToBottomVisibility: Boolean,
onSwipeToReply: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier,
+ knockRequestsBannerView: @Composable () -> Unit,
) {
Box(
modifier = modifier
- .fillMaxSize()
- .navigationBarsPadding()
- .imePadding(),
+ .fillMaxSize()
+ .navigationBarsPadding()
+ .imePadding(),
) {
AttachmentsBottomSheet(
state = state.composerState,
@@ -372,6 +375,7 @@ private fun MessagesViewContent(
onViewAllClick = onViewAllPinnedMessagesClick,
)
}
+ knockRequestsBannerView()
}
},
sheetContent = { subcomposing: Boolean ->
@@ -397,15 +401,15 @@ private fun MessagesViewComposerBottomSheetContents(
if (state.userEventPermissions.canSendMessage) {
Column(modifier = Modifier.fillMaxWidth()) {
SuggestionsPickerView(
- modifier = Modifier
- .heightIn(max = 230.dp)
- // Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
- .nestedScroll(object : NestedScrollConnection {
- override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
- return available
- }
- }),
isDebugBuild = state.isDebugBuild,
+ modifier = Modifier
+ .heightIn(max = 230.dp)
+ // Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
+ .nestedScroll(object : NestedScrollConnection {
+ override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
+ return available
+ }
+ }),
roomId = state.roomId,
roomName = state.roomName.dataOrNull(),
roomAvatarData = state.roomAvatar.dataOrNull(),
@@ -454,8 +458,8 @@ private fun MessagesViewTopBar(
title = {
val roundedCornerShape = RoundedCornerShape(8.dp)
val titleModifier = Modifier
- .clip(roundedCornerShape)
- .clickable { onRoomDetailsClick() }
+ .clip(roundedCornerShape)
+ .clickable { onRoomDetailsClick() }
if (roomName != null && roomAvatar != null) {
RoomAvatarAndNameRow(
roomName = roomName,
@@ -510,9 +514,9 @@ private fun RoomAvatarAndNameRow(
private fun CantSendMessageBanner() {
Row(
modifier = Modifier
- .fillMaxWidth()
- .background(MaterialTheme.colorScheme.secondary)
- .padding(16.dp),
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.secondary)
+ .padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
@@ -541,5 +545,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onJoinCallClick = {},
onViewAllPinnedMessagesClick = { },
forceJumpToBottomVisibility = true,
+ knockRequestsBannerView = {},
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt
index 411ff37c8f8..d631cf3dcd7 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt
@@ -37,6 +37,8 @@ import io.element.android.features.messages.impl.timeline.model.event.canBeCopie
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
import io.element.android.features.messages.impl.timeline.model.event.canReact
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
@@ -64,6 +66,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
private val room: MatrixRoom,
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
private val featureFlagService: FeatureFlagService,
+ private val dateFormatter: DateFormatter,
) : ActionListPresenter {
@AssistedFactory
@ContributesBinding(RoomScope::class)
@@ -131,6 +134,11 @@ class DefaultActionListPresenter @AssistedInject constructor(
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) {
target.value = ActionListState.Target.Success(
event = timelineItem,
+ sentTimeFull = dateFormatter.format(
+ timelineItem.sentTimeMillis,
+ DateFormatterMode.Full,
+ useRelative = true,
+ ),
displayEmojiReactions = displayEmojiReactions,
verifiedUserSendFailure = verifiedUserSendFailure,
actions = actions.toImmutableList()
@@ -170,6 +178,8 @@ class DefaultActionListPresenter @AssistedInject constructor(
add(TimelineItemAction.EditCaption)
add(TimelineItemAction.RemoveCaption)
}
+ } else if (timelineItem.content is TimelineItemPollContent) {
+ add(TimelineItemAction.EditPoll)
} else {
add(TimelineItemAction.Edit)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt
index 75c598df36d..56bc1ca0bd9 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt
@@ -24,6 +24,7 @@ data class ActionListState(
data class Loading(val event: TimelineItem.Event) : Target
data class Success(
val event: TimelineItem.Event,
+ val sentTimeFull: String,
val displayEmojiReactions: Boolean,
val verifiedUserSendFailure: VerifiedUserSendFailure,
val actions: ImmutableList,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt
index a5f027a5355..2fef1fc525b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt
@@ -37,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
event = aTimelineItemEvent(
timelineItemReactions = reactionsState
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
@@ -49,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
displayNameAmbiguous = true,
timelineItemReactions = reactionsState,
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(
@@ -62,6 +64,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
content = aTimelineItemVideoContent(),
timelineItemReactions = reactionsState
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(
@@ -75,6 +78,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
content = aTimelineItemFileContent(),
timelineItemReactions = reactionsState
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(
@@ -88,6 +92,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
content = aTimelineItemAudioContent(),
timelineItemReactions = reactionsState
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(
@@ -101,6 +106,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
content = aTimelineItemVoiceContent(caption = null),
timelineItemReactions = reactionsState
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(
@@ -114,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
content = aTimelineItemLocationContent(),
timelineItemReactions = reactionsState
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
@@ -125,6 +132,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
content = aTimelineItemLocationContent(),
timelineItemReactions = reactionsState
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
@@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
content = aTimelineItemPollContent(),
timelineItemReactions = reactionsState
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemPollActionList(),
@@ -147,6 +156,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
timelineItemReactions = reactionsState,
messageShield = MessageShield.UnknownDevice(isCritical = true)
),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
@@ -155,6 +165,7 @@ open class ActionListStateProvider : PreviewParameterProvider {
anActionListState(
target = ActionListState.Target.Success(
event = aTimelineItemEvent(),
+ sentTimeFull = "January 1, 1970 at 12:00 AM",
displayEmojiReactions = true,
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
actions = aTimelineItemActionList(),
@@ -192,6 +203,7 @@ fun aTimelineItemActionList(
fun aTimelineItemPollActionList(): ImmutableList {
return setOf(
TimelineItemAction.EndPoll,
+ TimelineItemAction.EditPoll,
TimelineItemAction.Reply,
TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
index 7d30edd1167..4cf0928d5c4 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
@@ -185,6 +185,7 @@ private fun ActionListViewContent(
Column {
MessageSummary(
event = target.event,
+ sentTimeFull = target.sentTimeFull,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
@@ -245,7 +246,11 @@ private fun ActionListViewContent(
@Suppress("MultipleEmitters") // False positive
@Composable
-private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
+private fun MessageSummary(
+ event: TimelineItem.Event,
+ sentTimeFull: String,
+ modifier: Modifier = Modifier,
+) {
val content: @Composable () -> Unit
val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) }
val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary)
@@ -300,20 +305,23 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
icon()
Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
- SenderName(
- senderId = event.senderId,
- senderProfile = event.senderProfile,
- senderNameMode = SenderNameMode.ActionList,
- )
+ Row {
+ SenderName(
+ modifier = Modifier.weight(1f),
+ senderId = event.senderId,
+ senderProfile = event.senderProfile,
+ senderNameMode = SenderNameMode.ActionList,
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(
+ text = sentTimeFull,
+ style = ElementTheme.typography.fontBodyXsRegular,
+ color = MaterialTheme.colorScheme.secondary,
+ textAlign = TextAlign.End,
+ )
+ }
content()
}
- Spacer(modifier = Modifier.width(16.dp))
- Text(
- event.sentTime,
- style = ElementTheme.typography.fontBodyXsRegular,
- color = MaterialTheme.colorScheme.secondary,
- textAlign = TextAlign.End,
- )
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt
index f700dcc6b1e..bd506fa3a53 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt
@@ -11,30 +11,30 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.icons.CompoundDrawables
-import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings
@Immutable
-sealed class TimelineItemAction(
+enum class TimelineItemAction(
@StringRes val titleRes: Int,
@DrawableRes val icon: Int,
val destructive: Boolean = false
) {
- data object ViewInTimeline : TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on)
- data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
- data object CopyText : TimelineItemAction(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy)
- data object CopyCaption : TimelineItemAction(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy)
- data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link)
- data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
- data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
- data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply)
- data object Edit : TimelineItemAction(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit)
- data object EditCaption : TimelineItemAction(CommonStrings.action_edit_caption, CompoundDrawables.ic_compound_edit)
- data object AddCaption : TimelineItemAction(CommonStrings.action_add_caption, CompoundDrawables.ic_compound_edit)
- data object RemoveCaption : TimelineItemAction(CommonStrings.action_remove_caption, CompoundDrawables.ic_compound_delete, destructive = true)
- data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
- data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
- data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
- data object Pin : TimelineItemAction(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin)
- data object Unpin : TimelineItemAction(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin)
+ ViewInTimeline(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on),
+ Forward(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward),
+ CopyText(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy),
+ CopyCaption(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy),
+ CopyLink(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link),
+ Redact(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true),
+ Reply(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply),
+ ReplyInThread(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply),
+ Edit(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit),
+ EditPoll(CommonStrings.action_edit_poll, CompoundDrawables.ic_compound_edit),
+ EditCaption(CommonStrings.action_edit_caption, CompoundDrawables.ic_compound_edit),
+ AddCaption(CommonStrings.action_add_caption, CompoundDrawables.ic_compound_edit),
+ RemoveCaption(CommonStrings.action_remove_caption, CompoundDrawables.ic_compound_close, destructive = true),
+ ViewSource(CommonStrings.action_view_source, CompoundDrawables.ic_compound_code),
+ ReportContent(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true),
+ EndPoll(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end),
+ Pin(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin),
+ Unpin(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin),
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt
index 8eef2d76199..a8a42b17ed1 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt
@@ -7,21 +7,25 @@
package io.element.android.features.messages.impl.actionlist.model
+import androidx.annotation.VisibleForTesting
+
class TimelineItemActionComparator : Comparator {
// See order in https://www.figma.com/design/ux3tYoZV9WghC7hHT9Fhk0/Compound-iOS-Components?node-id=2946-2392
- private val orderedList = listOf(
+ @VisibleForTesting
+ val orderedList = listOf(
TimelineItemAction.EndPoll,
TimelineItemAction.ViewInTimeline,
TimelineItemAction.Reply,
TimelineItemAction.ReplyInThread,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
- TimelineItemAction.Unpin,
- TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
- TimelineItemAction.CopyText,
+ TimelineItemAction.EditPoll,
TimelineItemAction.AddCaption,
TimelineItemAction.EditCaption,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
+ TimelineItemAction.Unpin,
+ TimelineItemAction.CopyText,
TimelineItemAction.CopyCaption,
TimelineItemAction.RemoveCaption,
TimelineItemAction.ViewSource,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt
index 34c58bdb060..5dc55b0dc4a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt
@@ -40,5 +40,6 @@ internal fun MessagesViewWithIdentityChangePreview(
onCreatePollClick = {},
onJoinCallClick = {},
onViewAllPinnedMessagesClick = {},
+ knockRequestsBannerView = {}
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt
index 48fdb83d796..1e86d4af089 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessor.kt
@@ -14,9 +14,9 @@ class PinnedMessagesListTimelineActionPostProcessor : TimelineItemActionPostProc
override fun process(actions: List): List {
return buildList {
add(TimelineItemAction.ViewInTimeline)
- actions.firstOrNull { it is TimelineItemAction.Unpin }?.let(::add)
- actions.firstOrNull { it is TimelineItemAction.Forward }?.let(::add)
- actions.firstOrNull { it is TimelineItemAction.ViewSource }?.let(::add)
+ actions.firstOrNull { it == TimelineItemAction.Unpin }?.let(::add)
+ actions.firstOrNull { it == TimelineItemAction.Forward }?.let(::add)
+ actions.firstOrNull { it == TimelineItemAction.ViewSource }?.let(::add)
}
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt
index b91b4ccc172..9ad875377c3 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt
@@ -40,9 +40,12 @@ fun TimelineItemEncryptedView(
UtdCause.UnknownDevice -> {
CommonStrings.common_unable_to_decrypt_insecure_device to CompoundDrawables.ic_compound_block
}
- UtdCause.HistoricalMessage -> {
+ UtdCause.HistoricalMessageAndBackupIsDisabled -> {
CommonStrings.timeline_decryption_failure_historical_event_no_key_backup to CompoundDrawables.ic_compound_block
}
+ UtdCause.HistoricalMessageAndDeviceIsUnverified -> {
+ CommonStrings.timeline_decryption_failure_historical_event_unverified_device to CompoundDrawables.ic_compound_block
+ }
UtdCause.WithheldUnverifiedOrInsecureDevice -> {
CommonStrings.timeline_decryption_failure_withheld_unverified to CompoundDrawables.ic_compound_block
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt
index 35a8cda2935..3fa786f902b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt
@@ -29,8 +29,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
-import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
@Composable
fun TimelineItemEventContentView(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt
index f5cee592e25..365b97f9fcc 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt
@@ -40,9 +40,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider
-import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents
-import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
-import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider
import io.element.android.libraries.androidutils.accessibility.isScreenReaderEnabled
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -52,6 +49,9 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
+import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
import kotlinx.coroutines.delay
@Composable
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt
index 28a0ff094f9..47b2b4eba51 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt
@@ -8,9 +8,9 @@
package io.element.android.features.messages.impl.timeline.di
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
-import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
-import io.element.android.features.messages.impl.voicemessages.timeline.aVoiceMessageState
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
+import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
/**
* A fake [TimelineItemPresenterFactories] for screenshot tests.
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
index d94ca9013a7..3700e02ccfa 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
@@ -20,7 +20,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
import io.element.android.libraries.core.bool.orTrue
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
@@ -32,14 +33,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua
import io.element.android.libraries.matrix.ui.messages.reply.map
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
-import java.text.DateFormat
import java.util.Date
class TimelineItemEventFactory @AssistedInject constructor(
@Assisted private val config: TimelineItemsFactoryConfig,
private val contentFactory: TimelineItemContentFactory,
private val matrixClient: MatrixClient,
- private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
+ private val dateFormatter: DateFormatter,
private val permalinkParser: PermalinkParser,
) {
@AssistedFactory
@@ -57,9 +57,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderProfile = currentTimelineItem.event.senderProfile
- val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
- val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
-
+ val sentTime = dateFormatter.format(
+ timestamp = currentTimelineItem.event.timestamp,
+ mode = DateFormatterMode.TimeOnly,
+ )
val senderAvatarData = AvatarData(
id = currentSender.value,
name = senderProfile.getDisambiguatedDisplayName(currentSender),
@@ -78,6 +79,7 @@ class TimelineItemEventFactory @AssistedInject constructor(
isMine = currentTimelineItem.event.isOwn,
isEditable = currentTimelineItem.event.isEditable,
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
+ sentTimeMillis = currentTimelineItem.event.timestamp,
sentTime = sentTime,
groupPosition = groupPosition,
reactionsState = currentTimelineItem.computeReactionsState(),
@@ -106,7 +108,6 @@ class TimelineItemEventFactory @AssistedInject constructor(
if (!config.computeReactions) {
return TimelineItemReactions(reactions = persistentListOf())
}
- val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
var aggregatedReactions = this.event.reactions.map { reaction ->
// Sort reactions within an aggregation by timestamp descending.
// This puts the most recent at the top, useful in cases like the
@@ -121,7 +122,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
AggregatedReactionSender(
senderId = it.senderId,
timestamp = date,
- sentTime = timeFormatter.format(date),
+ sentTime = dateFormatter.format(
+ it.timestamp,
+ DateFormatterMode.TimeOrDate,
+ ),
)
}
.toImmutableList()
@@ -157,7 +161,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
url = roomMember?.avatarUrl,
size = AvatarSize.TimelineReadReceipt,
),
- formattedDate = lastMessageTimestampFormatter.format(receipt.timestamp)
+ formattedDate = dateFormatter.format(
+ receipt.timestamp,
+ mode = DateFormatterMode.TimeOrDate,
+ )
)
}
.toImmutableList()
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt
index 41966c036bd..cd680d4e809 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt
@@ -9,13 +9,20 @@ package io.element.android.features.messages.impl.timeline.factories.virtual
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
-import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import javax.inject.Inject
-class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) {
+class TimelineItemDaySeparatorFactory @Inject constructor(
+ private val dateFormatter: DateFormatter,
+) {
fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel {
- val formattedDate = daySeparatorFormatter.format(virtualItem.timestamp)
+ val formattedDate = dateFormatter.format(
+ timestamp = virtualItem.timestamp,
+ mode = DateFormatterMode.Day,
+ useRelative = true,
+ )
return TimelineItemDaySeparatorModel(
formattedDate = formattedDate
)
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 0a392aac6a0..53237ef4dec 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
@@ -71,6 +71,7 @@ sealed interface TimelineItem {
val senderProfile: ProfileTimelineDetails,
val senderAvatar: AvatarData,
val content: TimelineItemEventContent,
+ val sentTimeMillis: Long = 0L,
val sentTime: String = "",
val isMine: Boolean = false,
val isEditable: Boolean,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentProvider.kt
index b312024ebb7..d34f63ecbde 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentProvider.kt
@@ -36,7 +36,13 @@ open class TimelineItemEncryptedContentProvider : PreviewParameterProvider {
@AssistedFactory
@@ -55,97 +41,16 @@ class VoiceMessagePresenter @AssistedInject constructor(
override fun create(content: TimelineItemVoiceContent): VoiceMessagePresenter
}
- private val player = voiceMessagePlayerFactory.create(
+ private val presenter = voiceMessagePresenterFactory.createVoiceMessagePresenter(
eventId = content.eventId,
mediaSource = content.mediaSource,
mimeType = content.mimeType,
filename = content.filename,
+ duration = content.duration,
)
- private val play = mutableStateOf>(AsyncData.Uninitialized)
-
@Composable
override fun present(): VoiceMessageState {
- val playerState by player.state.collectAsState(
- VoiceMessagePlayer.State(
- isReady = false,
- isPlaying = false,
- isEnded = false,
- currentPosition = 0L,
- duration = null
- )
- )
-
- val button by remember {
- derivedStateOf {
- when {
- content.eventId == null -> VoiceMessageState.Button.Disabled
- playerState.isPlaying -> VoiceMessageState.Button.Pause
- play.value is AsyncData.Loading -> VoiceMessageState.Button.Downloading
- play.value is AsyncData.Failure -> VoiceMessageState.Button.Retry
- else -> VoiceMessageState.Button.Play
- }
- }
- }
- val duration by remember {
- derivedStateOf { playerState.duration ?: content.duration.inWholeMilliseconds }
- }
- val progress by remember {
- derivedStateOf {
- playerState.currentPosition / duration.toFloat()
- }
- }
- val time by remember {
- derivedStateOf {
- when {
- playerState.isReady && !playerState.isEnded -> playerState.currentPosition
- playerState.currentPosition > 0 -> playerState.currentPosition
- else -> duration
- }.milliseconds.formatShort()
- }
- }
- val showCursor by remember {
- derivedStateOf {
- !play.value.isUninitialized() && !playerState.isEnded
- }
- }
-
- fun eventSink(event: VoiceMessageEvents) {
- when (event) {
- is VoiceMessageEvents.PlayPause -> {
- if (playerState.isPlaying) {
- player.pause()
- } else if (playerState.isReady) {
- player.play()
- } else {
- scope.launch {
- play.runUpdatingState(
- errorTransform = {
- analyticsService.trackError(
- VoiceMessageException.PlayMessageError("Error while trying to play voice message", it)
- )
- it
- },
- ) {
- player.prepare().flatMap {
- runCatching { player.play() }
- }
- }
- }
- }
- }
- is VoiceMessageEvents.Seek -> {
- player.seekTo((event.percentage * duration).toLong())
- }
- }
- }
-
- return VoiceMessageState(
- button = button,
- progress = progress,
- time = time,
- showCursor = showCursor,
- eventSink = { eventSink(it) },
- )
+ return presenter.present()
}
}
diff --git a/features/messages/impl/src/main/res/values-hu/translations.xml b/features/messages/impl/src/main/res/values-hu/translations.xml
index 4c2a66334fd..6fce42474cd 100644
--- a/features/messages/impl/src/main/res/values-hu/translations.xml
+++ b/features/messages/impl/src/main/res/values-hu/translations.xml
@@ -31,6 +31,7 @@
"Emodzsi hozzáadása"
"Ez a(z) %1$s kezdete."
"Ez a beszélgetés kezdete."
+ "Nem támogatott hívás. Kérdezze meg, hogy a hívó fél tudja-e használni az új Element X alkalmazást."
"Kevesebb megjelenítése"
"Üzenet másolva"
"Nincs jogosultsága arra, hogy bejegyzést tegyen közzé ebben a szobában"
diff --git a/features/messages/impl/src/main/res/values-it/translations.xml b/features/messages/impl/src/main/res/values-it/translations.xml
index c1acba9be80..229a02b714a 100644
--- a/features/messages/impl/src/main/res/values-it/translations.xml
+++ b/features/messages/impl/src/main/res/values-it/translations.xml
@@ -31,6 +31,7 @@
"Aggiungi emoji"
"Questo è l\'inizio di %1$s."
"Questo è l\'inizio della conversazione."
+ "Chiamata non supportata. Chiedi se il chiamante può utilizzare la nuova app Element X."
"Mostra meno"
"Messaggio copiato"
"Non sei autorizzato a postare in questa stanza"
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 8cb7464a9ad..1369b5af635 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
@@ -467,7 +467,7 @@ class MessagesPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Edit, aMessageEvent(content = aTimelineItemPollContent())))
+ initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EditPoll, aMessageEvent(content = aTimelineItemPollContent())))
awaitItem()
onEditPollClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt
index 1d4e1a43b34..c8305f971be 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt
@@ -327,6 +327,7 @@ class MessagesViewTest {
actionListState = anActionListState(
target = ActionListState.Target.Success(
event = timelineItem,
+ sentTimeFull = "",
displayEmojiReactions = true,
actions = persistentListOf(TimelineItemAction.Edit),
verifiedUserSendFailure = VerifiedUserSendFailure.None,
@@ -399,6 +400,7 @@ class MessagesViewTest {
actionListState = anActionListState(
target = ActionListState.Target.Success(
event = timelineItem,
+ sentTimeFull = "",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(TimelineItemAction.Edit),
@@ -427,6 +429,7 @@ class MessagesViewTest {
actionListState = anActionListState(
target = ActionListState.Target.Success(
event = timelineItem,
+ sentTimeFull = "",
displayEmojiReactions = true,
verifiedUserSendFailure = aChangedIdentitySendFailure(),
actions = persistentListOf(),
@@ -533,6 +536,7 @@ private fun AndroidComposeTestRule.setMessa
onCreatePollClick = onCreatePollClick,
onJoinCallClick = onJoinCallClick,
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
+ knockRequestsBannerView = {}
)
}
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt
index 49db6f6c955..cc4120ffab1 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt
@@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.MatrixRoom
@@ -86,6 +87,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
@@ -128,6 +130,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
@@ -170,13 +173,14 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
@@ -215,13 +219,14 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.ReplyInThread,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
@@ -263,12 +268,13 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
@@ -308,13 +314,14 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
@@ -355,13 +362,14 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
@@ -403,14 +411,15 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
- TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
@@ -448,14 +457,15 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.ReplyInThread,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
- TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
@@ -496,14 +506,15 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
- TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
)
@@ -542,14 +553,15 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
- TimelineItemAction.CopyLink,
TimelineItemAction.AddCaption,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
@@ -592,6 +604,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
@@ -599,8 +612,8 @@ class ActionListPresenterTest {
TimelineItemAction.Forward,
// Not here
// TimelineItemAction.AddCaption,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
@@ -641,14 +654,15 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
- TimelineItemAction.CopyLink,
TimelineItemAction.EditCaption,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyCaption,
TimelineItemAction.RemoveCaption,
TimelineItemAction.ViewSource,
@@ -691,13 +705,14 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyCaption,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
@@ -738,6 +753,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = stateEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
@@ -808,14 +824,15 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
- TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.CopyText,
TimelineItemAction.Redact,
)
@@ -855,13 +872,14 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
+ TimelineItemAction.CopyLink,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
@@ -909,14 +927,15 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Unpin,
- TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
+ TimelineItemAction.CopyLink,
+ TimelineItemAction.Unpin,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
@@ -1006,6 +1025,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
@@ -1046,14 +1066,15 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.EndPoll,
TimelineItemAction.Reply,
- TimelineItemAction.Pin,
+ TimelineItemAction.EditPoll,
TimelineItemAction.CopyLink,
- TimelineItemAction.Edit,
+ TimelineItemAction.Pin,
TimelineItemAction.Redact,
)
)
@@ -1089,13 +1110,14 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.EndPoll,
TimelineItemAction.Reply,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.Redact,
)
)
@@ -1131,12 +1153,13 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.Redact,
)
)
@@ -1174,13 +1197,14 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
- TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
+ TimelineItemAction.Pin,
TimelineItemAction.Redact,
)
)
@@ -1214,6 +1238,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
+ sentTimeFull = "0 Full true",
displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
@@ -1268,6 +1293,7 @@ private fun createActionListPresenter(
initialState = mapOf(
FeatureFlags.MediaCaptionCreation.key to allowCaption,
),
- )
+ ),
+ dateFormatter = FakeDateFormatter(),
)
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparatorTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparatorTest.kt
new file mode 100644
index 00000000000..9866d846ec3
--- /dev/null
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparatorTest.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.features.messages.impl.actionlist.model
+
+import org.junit.Test
+
+class TimelineItemActionComparatorTest {
+ @Test
+ fun `check that the list in the comparator only contain each item once`() {
+ val sut = TimelineItemActionComparator()
+ sut.orderedList.forEach {
+ require(sut.orderedList.count { item -> item == it } == 1, { "Duplicate ${it::class.java}.$it" })
+ }
+ }
+
+ @Test
+ fun `check that the list in the comparator contains all the items`() {
+ val sut = TimelineItemActionComparator()
+ TimelineItemAction.entries.forEach {
+ require(it in sut.orderedList, { "Missing ${it::class.simpleName}.$it in orderedList" })
+ }
+ }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt
index 51c4cb43ba0..df76e15b6c3 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt
@@ -28,8 +28,7 @@ import io.element.android.features.messages.impl.utils.FakeTextPillificationHelp
import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider
import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
-import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
-import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
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
@@ -80,7 +79,7 @@ internal fun TestScope.aTimelineItemsFactory(
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
),
matrixClient = matrixClient,
- lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
+ dateFormatter = FakeDateFormatter(),
permalinkParser = FakePermalinkParser(),
config = config
)
@@ -88,7 +87,7 @@ internal fun TestScope.aTimelineItemsFactory(
},
virtualItemFactory = TimelineItemVirtualFactory(
daySeparatorFactory = TimelineItemDaySeparatorFactory(
- FakeDaySeparatorFormatter()
+ FakeDateFormatter()
),
),
timelineItemGrouper = TimelineItemGrouper(),
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessorTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessorTest.kt
index 7043d3f8484..59545d66744 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessorTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListTimelineActionPostProcessorTest.kt
@@ -27,24 +27,7 @@ class PinnedMessagesListTimelineActionPostProcessorTest {
fun `ensure that some actions are kept and some other are filtered out`() {
val sut = PinnedMessagesListTimelineActionPostProcessor()
val result = sut.process(
- listOf(
- TimelineItemAction.Forward,
- TimelineItemAction.CopyText,
- TimelineItemAction.CopyCaption,
- TimelineItemAction.CopyLink,
- TimelineItemAction.Redact,
- TimelineItemAction.Reply,
- TimelineItemAction.ReplyInThread,
- TimelineItemAction.Edit,
- TimelineItemAction.EditCaption,
- TimelineItemAction.AddCaption,
- TimelineItemAction.RemoveCaption,
- TimelineItemAction.ViewSource,
- TimelineItemAction.ReportContent,
- TimelineItemAction.EndPoll,
- TimelineItemAction.Pin,
- TimelineItemAction.Unpin,
- )
+ TimelineItemAction.entries.toList()
)
assertThat(result).isEqualTo(
listOf(
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
index a7cbaaffd2e..f1389962b05 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
@@ -18,7 +18,6 @@ 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.messagecomposer.aReplyMode
-import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.media.AudioInfo
@@ -36,6 +35,7 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
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.voiceplayer.api.VoiceMessageException
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml
index 56edc949a13..bd5e9c0c544 100644
--- a/features/onboarding/impl/src/main/res/values-de/translations.xml
+++ b/features/onboarding/impl/src/main/res/values-de/translations.xml
@@ -5,5 +5,5 @@
"Konto erstellen"
"Willkommen beim schnellsten %1$s aller Zeiten. Optimiert für Geschwindigkeit und Einfachheit."
"Willkommen zu %1$s. Aufgeladen, für Geschwindigkeit und Einfachheit."
- "Sei in deinem Element"
+ "Sei in Deinem Element"
diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt
index 60814477c9e..1c667efffbb 100644
--- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt
+++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt
@@ -9,7 +9,8 @@ package io.element.android.features.poll.impl.history.model
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import kotlinx.collections.immutable.toPersistentList
@@ -18,7 +19,7 @@ import javax.inject.Inject
class PollHistoryItemsFactory @Inject constructor(
private val pollContentStateFactory: PollContentStateFactory,
- private val daySeparatorFormatter: DaySeparatorFormatter,
+ private val dateFormatter: DateFormatter,
private val dispatchers: CoroutineDispatchers,
) {
suspend fun create(timelineItems: List): PollHistoryItems = withContext(dispatchers.computation) {
@@ -45,7 +46,11 @@ class PollHistoryItemsFactory @Inject constructor(
val pollContent = timelineItem.event.content as? PollContent ?: return null
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
PollHistoryItem(
- formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp),
+ formattedDate = dateFormatter.format(
+ timestamp = timelineItem.event.timestamp,
+ mode = DateFormatterMode.Day,
+ useRelative = true
+ ),
state = pollContentState
)
}
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 081b5e53b14..8a0c8ea5d56 100644
--- a/features/poll/impl/src/main/res/values-fr/translations.xml
+++ b/features/poll/impl/src/main/res/values-fr/translations.xml
@@ -4,11 +4,11 @@
"Afficher les résultats uniquement après la fin du sondage"
"Masquer les votes"
"Option %1$d"
- "Vos modifications n’ont pas été enregistrées. Êtes-vous certain de vouloir quitter?"
+ "Vos modifications n’ont pas été enregistrées. Êtes-vous certain de vouloir quitter ?"
"Question ou sujet"
"Quel est le sujet du sondage ?"
"Créer un sondage"
- "Êtes-vous certain de vouloir supprimer ce sondage?"
+ "Êtes-vous certain de vouloir supprimer ce sondage ?"
"Supprimer le sondage"
"Modifier le sondage"
"Impossible de trouver des sondages en cours."
diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
index 6dfa8df752e..d3e67e223ed 100644
--- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
+++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
@@ -21,7 +21,7 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItemsFacto
import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory
import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
-import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.AN_EVENT_ID
@@ -161,7 +161,7 @@ class PollHistoryPresenterTest {
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()),
- daySeparatorFormatter = FakeDaySeparatorFormatter(),
+ dateFormatter = FakeDateFormatter(),
dispatchers = testCoroutineDispatchers(),
),
): PollHistoryPresenter {
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
index 844474e1e14..c4896ca5dd3 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt
@@ -23,6 +23,7 @@ import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@@ -63,7 +64,7 @@ class DeveloperSettingsPresenter @Inject constructor(
mutableStateOf>(AsyncData.Uninitialized)
}
val clearCacheAction = remember {
- mutableStateOf>(AsyncData.Uninitialized)
+ mutableStateOf>(AsyncAction.Uninitialized)
}
val customElementCallBaseUrl by appPreferencesStore
.getCustomElementCallBaseUrlFlow()
@@ -94,7 +95,7 @@ class DeveloperSettingsPresenter @Inject constructor(
val featureUiModels = createUiModels(features, enabledFeatures)
val coroutineScope = rememberCoroutineScope()
// Compute cache size each time the clear cache action value is changed
- LaunchedEffect(clearCacheAction.value) {
+ LaunchedEffect(clearCacheAction.value.isSuccess()) {
computeCacheSize(cacheSize)
}
@@ -180,7 +181,7 @@ class DeveloperSettingsPresenter @Inject constructor(
}.runCatchingUpdatingState(cacheSize)
}
- private fun CoroutineScope.clearCache(clearCacheAction: MutableState>) = launch {
+ private fun CoroutineScope.clearCache(clearCacheAction: MutableState>) = launch {
suspend {
clearCacheUseCase()
}.runCatchingUpdatingState(clearCacheAction)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt
index e4c86411978..7c2b9438ae5 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt
@@ -8,6 +8,7 @@
package io.element.android.features.preferences.impl.developer
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import kotlinx.collections.immutable.ImmutableList
@@ -16,7 +17,7 @@ data class DeveloperSettingsState(
val features: ImmutableList,
val cacheSize: AsyncData,
val rageshakeState: RageshakePreferencesState,
- val clearCacheAction: AsyncData,
+ val clearCacheAction: AsyncAction,
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val isSimpleSlidingSyncEnabled: Boolean,
val hideImagesAndVideos: Boolean,
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
index 601ed2ee7a3..8742e4746d7 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt
@@ -9,6 +9,7 @@ package io.element.android.features.preferences.impl.developer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList
@@ -17,7 +18,7 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider = AsyncData.Uninitialized,
+ clearCacheAction: AsyncAction = AsyncAction.Uninitialized,
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
isSimplifiedSlidingSyncEnabled: Boolean = false,
hideImagesAndVideos: Boolean = false,
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
index d0b77314426..a8c736d3f04 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt
@@ -25,6 +25,7 @@ import io.element.android.features.preferences.impl.user.UserPreferences
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
+import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
@@ -33,7 +34,6 @@ import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
-import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.DeviceId
@@ -271,7 +271,7 @@ private fun ColumnScope.Footer(
private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) {
ListItem(
headlineContent = { Text(stringResource(id = CommonStrings.common_developer_options)) },
- leadingContent = ListItemContent.Icon(IconSource.Resource(CommonDrawables.ic_developer_options)),
+ leadingContent = ListItemContent.Icon(IconSource.Resource(CompoundDrawables.ic_compound_code)),
onClick = onOpenDeveloperSettings
)
}
diff --git a/features/preferences/impl/src/main/res/values-it/translations.xml b/features/preferences/impl/src/main/res/values-it/translations.xml
index 5acfeff05bd..e47167e32b6 100644
--- a/features/preferences/impl/src/main/res/values-it/translations.xml
+++ b/features/preferences/impl/src/main/res/values-it/translations.xml
@@ -8,6 +8,8 @@
"URL base di Element Call personalizzato"
"Imposta un URL di base personalizzato per Element Call."
"URL non valido, assicurati di includere il protocollo (http/https) e l\'indirizzo corretto."
+ "Carica foto e video più velocemente e riduci l\'utilizzo dei dati"
+ "Ottimizza la qualità dei contenuti multimediali"
"Fornitore di notifiche push"
"Disattiva l\'editor di testo avanzato per scrivere manualmente in Markdown"
"Ricevute di visualizzazione"
diff --git a/features/preferences/impl/src/main/res/values-pt-rBR/translations.xml b/features/preferences/impl/src/main/res/values-pt-rBR/translations.xml
index 6255c7c2c96..42ac543aee1 100644
--- a/features/preferences/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/preferences/impl/src/main/res/values-pt-rBR/translations.xml
@@ -3,6 +3,8 @@
"Escolha como receber notificações"
"Modo de desenvolvedor"
"Habilite para ter acesso a recursos e funcionalidades para desenvolvedores."
+ "URL base do Element Call personalizado"
+ "Defina um URL base personalizado para Element Call."
"URL inválida, por favor verifique se o protocolo (http/https) e o endereço correto estão presentes."
"Desative o editor de rich text para digitar Markdown manualmente."
"Confirmações de leitura"
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
index 5568b2beba8..a31d1c032ad 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt
@@ -5,17 +5,17 @@
* Please see LICENSE in the repository root for full details.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package io.element.android.features.preferences.impl.developer
-import app.cash.molecule.RecompositionMode
-import app.cash.molecule.moleculeFlow
-import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.logout.test.FakeLogoutUseCase
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
@@ -24,8 +24,8 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
-import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.advanceUntilIdle
@@ -38,37 +38,29 @@ class DeveloperSettingsPresenterTest {
val warmUpRule = WarmUpRule()
@Test
- fun `present - ensures initial state is correct`() = runTest {
- val presenter = createDeveloperSettingsPresenter()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitItem()
- assertThat(initialState.features).isEmpty()
- assertThat(initialState.clearCacheAction).isEqualTo(AsyncData.Uninitialized)
- assertThat(initialState.cacheSize).isEqualTo(AsyncData.Uninitialized)
- assertThat(initialState.customElementCallBaseUrlState).isNotNull()
- assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
- assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
- assertThat(initialState.hideImagesAndVideos).isFalse()
- val loadedState = awaitItem()
- assertThat(loadedState.rageshakeState.isEnabled).isFalse()
- assertThat(loadedState.rageshakeState.isSupported).isTrue()
- assertThat(loadedState.rageshakeState.sensitivity).isEqualTo(0.3f)
- cancelAndIgnoreRemainingEvents()
- }
- }
-
- @Test
- fun `present - ensures feature list is loaded`() = runTest {
+ fun `present - ensures initial states are correct`() = runTest {
val presenter = createDeveloperSettingsPresenter()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val state = awaitLastSequentialItem()
- val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() }
- assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
- cancelAndIgnoreRemainingEvents()
+ presenter.test {
+ awaitItem().also { state ->
+ assertThat(state.features).isEmpty()
+ assertThat(state.clearCacheAction).isEqualTo(AsyncAction.Uninitialized)
+ assertThat(state.cacheSize).isEqualTo(AsyncData.Uninitialized)
+ assertThat(state.customElementCallBaseUrlState).isNotNull()
+ assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
+ assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
+ assertThat(state.hideImagesAndVideos).isFalse()
+ assertThat(state.rageshakeState.isEnabled).isFalse()
+ assertThat(state.rageshakeState.isSupported).isTrue()
+ assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f)
+ }
+ awaitItem().also { state ->
+ assertThat(state.features).isNotEmpty()
+ val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() }
+ assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
+ }
+ awaitItem().also { state ->
+ assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java)
+ }
}
}
@@ -76,30 +68,28 @@ class DeveloperSettingsPresenterTest {
fun `present - ensures Room directory search is not present on release Google Play builds`() = runTest {
val buildMeta = aBuildMeta(buildType = BuildType.RELEASE, flavorDescription = "GooglePlay")
val presenter = createDeveloperSettingsPresenter(buildMeta = buildMeta)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val state = awaitLastSequentialItem()
- assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch)
- cancelAndIgnoreRemainingEvents()
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.features).doesNotContain(FeatureFlags.RoomDirectorySearch)
+ }
}
}
@Test
fun `present - ensures state is updated when enabled feature event is triggered`() = runTest {
val presenter = createDeveloperSettingsPresenter()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- skipItems(1)
- val stateBeforeEvent = awaitItem()
- val featureBeforeEvent = stateBeforeEvent.features.first()
- stateBeforeEvent.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(featureBeforeEvent, !featureBeforeEvent.isEnabled))
- val stateAfterEvent = awaitItem()
- val featureAfterEvent = stateAfterEvent.features.first()
- assertThat(featureBeforeEvent.key).isEqualTo(featureAfterEvent.key)
- assertThat(featureBeforeEvent.isEnabled).isNotEqualTo(featureAfterEvent.isEnabled)
- cancelAndIgnoreRemainingEvents()
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ val feature = state.features.first()
+ state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, !feature.isEnabled))
+ }
+ awaitItem().also { state ->
+ val feature = state.features.first()
+ assertThat(feature.isEnabled).isTrue()
+ assertThat(feature.key).isEqualTo(feature.key)
+ }
}
}
@@ -107,19 +97,25 @@ class DeveloperSettingsPresenterTest {
fun `present - clear cache`() = runTest {
val clearCacheUseCase = FakeClearCacheUseCase()
val presenter = createDeveloperSettingsPresenter(clearCacheUseCase = clearCacheUseCase)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- skipItems(1)
- val initialState = awaitItem()
+ presenter.test {
+ skipItems(2)
assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse()
- initialState.eventSink(DeveloperSettingsEvents.ClearCache)
- val stateAfterEvent = awaitItem()
- assertThat(stateAfterEvent.clearCacheAction).isInstanceOf(AsyncData.Loading::class.java)
- skipItems(1)
- assertThat(awaitItem().clearCacheAction).isInstanceOf(AsyncData.Success::class.java)
- assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
- cancelAndIgnoreRemainingEvents()
+ awaitItem().also { state ->
+ state.eventSink(DeveloperSettingsEvents.ClearCache)
+ }
+ awaitItem().also { state ->
+ assertThat(state.clearCacheAction).isInstanceOf(AsyncAction.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.clearCacheAction).isInstanceOf(AsyncAction.Success::class.java)
+ assertThat(clearCacheUseCase.executeHasBeenCalled).isTrue()
+ }
+ awaitItem().also { state ->
+ assertThat(state.cacheSize).isInstanceOf(AsyncData.Loading::class.java)
+ }
+ awaitItem().also { state ->
+ assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java)
+ }
}
}
@@ -127,26 +123,25 @@ class DeveloperSettingsPresenterTest {
fun `present - custom element call base url`() = runTest {
val preferencesStore = InMemoryAppPreferencesStore()
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- skipItems(1)
- val initialState = awaitItem()
- assertThat(initialState.customElementCallBaseUrlState.baseUrl).isNull()
- initialState.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy"))
- val updatedItem = awaitItem()
- assertThat(updatedItem.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
- assertThat(updatedItem.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL)
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.customElementCallBaseUrlState.baseUrl).isNull()
+ state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.ahoy"))
+ }
+ awaitItem().also { state ->
+ assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
+ assertThat(state.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL)
+ }
}
}
@Test
fun `present - custom element call base url validator needs at least an HTTP scheme and host`() = runTest {
val presenter = createDeveloperSettingsPresenter()
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val urlValidator = awaitLastSequentialItem().customElementCallBaseUrlState.validator
+ presenter.test {
+ skipItems(2)
+ val urlValidator = awaitItem().customElementCallBaseUrlState.validator
assertThat(urlValidator("")).isTrue() // We allow empty string to clear the value and use the default one
assertThat(urlValidator("test")).isFalse()
assertThat(urlValidator("http://")).isFalse()
@@ -155,30 +150,31 @@ class DeveloperSettingsPresenterTest {
}
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - toggling simplified sliding sync changes the preferences and logs out the user`() = runTest {
val logoutCallRecorder = lambdaRecorder { "" }
val logoutUseCase = FakeLogoutUseCase(logoutLambda = logoutCallRecorder)
val preferences = InMemoryAppPreferencesStore()
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences, logoutUseCase = logoutUseCase)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitLastSequentialItem()
- assertThat(initialState.isSimpleSlidingSyncEnabled).isFalse()
-
- initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
- assertThat(awaitItem().isSimpleSlidingSyncEnabled).isTrue()
- assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
- advanceUntilIdle()
- logoutCallRecorder.assertions().isCalledOnce()
-
- initialState.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false))
- assertThat(awaitItem().isSimpleSlidingSyncEnabled).isFalse()
- assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
- advanceUntilIdle()
- logoutCallRecorder.assertions().isCalledExactly(times = 2)
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
+ state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(true))
+ }
+ awaitItem().also { state ->
+ assertThat(state.isSimpleSlidingSyncEnabled).isTrue()
+ assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isTrue()
+ advanceUntilIdle()
+ logoutCallRecorder.assertions().isCalledOnce()
+ state.eventSink(DeveloperSettingsEvents.SetSimplifiedSlidingSyncEnabled(false))
+ }
+ awaitItem().also { state ->
+ assertThat(state.isSimpleSlidingSyncEnabled).isFalse()
+ assertThat(preferences.isSimplifiedSlidingSyncEnabledFlow().first()).isFalse()
+ advanceUntilIdle()
+ logoutCallRecorder.assertions().isCalledExactly(2)
+ }
}
}
@@ -186,17 +182,21 @@ class DeveloperSettingsPresenterTest {
fun `present - toggling hide image and video`() = runTest {
val preferences = InMemoryAppPreferencesStore()
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences)
- moleculeFlow(RecompositionMode.Immediate) {
- presenter.present()
- }.test {
- val initialState = awaitLastSequentialItem()
- assertThat(initialState.hideImagesAndVideos).isFalse()
- initialState.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
- assertThat(awaitItem().hideImagesAndVideos).isTrue()
- assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
- initialState.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false))
- assertThat(awaitItem().hideImagesAndVideos).isFalse()
- assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
+ presenter.test {
+ skipItems(2)
+ awaitItem().also { state ->
+ assertThat(state.hideImagesAndVideos).isFalse()
+ state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(true))
+ }
+ awaitItem().also { state ->
+ assertThat(state.hideImagesAndVideos).isTrue()
+ assertThat(preferences.doesHideImagesAndVideosFlow().first()).isTrue()
+ state.eventSink(DeveloperSettingsEvents.SetHideImagesAndVideos(false))
+ }
+ awaitItem().also { state ->
+ assertThat(state.hideImagesAndVideos).isFalse()
+ assertThat(preferences.doesHideImagesAndVideosFlow().first()).isFalse()
+ }
}
}
diff --git a/features/rageshake/api/src/main/res/values-fr/translations.xml b/features/rageshake/api/src/main/res/values-fr/translations.xml
index e21171120e3..a2d13912802 100644
--- a/features/rageshake/api/src/main/res/values-fr/translations.xml
+++ b/features/rageshake/api/src/main/res/values-fr/translations.xml
@@ -1,7 +1,7 @@
"%1$s s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?"
- "Vous semblez secouez votre téléphone avec frustration. Souhaitez-vous ouvrir le formulaire pour reporter un problème?"
+ "Vous semblez secouez votre téléphone avec frustration. Souhaitez-vous ouvrir le formulaire pour reporter un problème ?"
"Rageshake"
"Seuil de détection"
diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts
index 231161e583a..bfb2cfde7ad 100644
--- a/features/roomdetails/impl/build.gradle.kts
+++ b/features/roomdetails/impl/build.gradle.kts
@@ -50,6 +50,7 @@ dependencies {
implementation(projects.features.poll.api)
implementation(projects.features.messages.api)
implementation(projects.features.roomcall.api)
+ implementation(projects.features.knockrequests.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
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 12cdbdcadf7..f89953263cc 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
@@ -15,6 +15,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@@ -22,6 +23,7 @@ import im.vector.app.features.analytics.plan.Interaction
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint
+import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
@@ -38,10 +40,13 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.overlay.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
@@ -56,7 +61,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
private val room: MatrixRoom,
private val analyticsService: AnalyticsService,
private val messagesEntryPoint: MessagesEntryPoint,
+ private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
+ private val mediaGalleryEntryPoint: MediaGalleryEntryPoint,
) : BaseFlowNode(
backstack = BackStack(
initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(),
@@ -96,11 +103,17 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize
data object PollHistory : NavTarget
+ @Parcelize
+ data object MediaGallery : NavTarget
+
@Parcelize
data object AdminSettings : NavTarget
@Parcelize
data object PinnedMessagesList : NavTarget
+
+ @Parcelize
+ data object KnockRequestsList : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -131,6 +144,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
backstack.push(NavTarget.PollHistory)
}
+ override fun openMediaGallery() {
+ backstack.push(NavTarget.MediaGallery)
+ }
+
override fun openAdminSettings() {
backstack.push(NavTarget.AdminSettings)
}
@@ -139,6 +156,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
backstack.push(NavTarget.PinnedMessagesList)
}
+ override fun openKnockRequestsList() {
+ backstack.push(NavTarget.KnockRequestsList)
+ }
+
override fun onJoinCall() {
val inputs = CallType.RoomCall(
sessionId = room.sessionId,
@@ -204,6 +225,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
override fun onDone() {
overlay.hide()
}
+
+ override fun onViewInTimeline(eventId: EventId) {
+ // Cannot happen
+ }
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
.avatar(
@@ -213,10 +238,29 @@ class RoomDetailsFlowNode @AssistedInject constructor(
.callback(callback)
.build()
}
-
is NavTarget.PollHistory -> {
pollHistoryEntryPoint.createNode(this, buildContext)
}
+ is NavTarget.MediaGallery -> {
+ val callback = object : MediaGalleryEntryPoint.Callback {
+ override fun onBackClick() {
+ backstack.pop()
+ }
+
+ override fun onViewInTimeline(eventId: EventId) {
+ val permalinkData = PermalinkData.RoomLink(
+ roomIdOrAlias = room.roomId.toRoomIdOrAlias(),
+ eventId = eventId,
+ )
+ plugins().forEach {
+ it.onPermalinkClick(permalinkData, pushToBackstack = false)
+ }
+ }
+ }
+ mediaGalleryEntryPoint.nodeBuilder(this, buildContext)
+ .callback(callback)
+ .build()
+ }
is NavTarget.AdminSettings -> {
createNode(buildContext)
@@ -243,6 +287,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
.callback(callback)
.build()
}
+ NavTarget.KnockRequestsList -> {
+ knockRequestsListEntryPoint.createNode(this, buildContext)
+ }
}
}
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 19c0b4ffe4e..3b3d11fbd92 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
@@ -45,8 +45,10 @@ class RoomDetailsNode @AssistedInject constructor(
fun openRoomNotificationSettings()
fun openAvatarPreview(name: String, url: String)
fun openPollHistory()
+ fun openMediaGallery()
fun openAdminSettings()
fun openPinnedMessagesList()
+ fun openKnockRequestsList()
fun onJoinCall()
}
@@ -76,6 +78,10 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.openPollHistory() }
}
+ private fun openMediaGallery() {
+ callbacks.forEach { it.openMediaGallery() }
+ }
+
private fun onJoinCall() {
callbacks.forEach { it.onJoinCall() }
}
@@ -111,6 +117,10 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.openPinnedMessagesList() }
}
+ private fun openKnockRequestsLists() {
+ callbacks.forEach { it.openKnockRequestsList() }
+ }
+
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
@@ -138,9 +148,11 @@ class RoomDetailsNode @AssistedInject constructor(
invitePeople = ::invitePeople,
openAvatarPreview = ::openAvatarPreview,
openPollHistory = ::openPollHistory,
+ openMediaGallery = ::openMediaGallery,
openAdminSettings = this::openAdminSettings,
onJoinCallClick = ::onJoinCall,
- onPinnedMessagesClick = ::openPinnedMessages
+ onPinnedMessagesClick = ::openPinnedMessages,
+ onKnockRequestsClick = ::openKnockRequestsLists,
)
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
index b1940d424e1..345321ec5cb 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
@@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
@@ -36,9 +37,11 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.isDm
+import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
+import io.element.android.libraries.matrix.ui.room.canHandleKnockRequestsAsState
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
@@ -71,15 +74,19 @@ class RoomDetailsPresenter @Inject constructor(
val canShowNotificationSettings = remember { mutableStateOf(false) }
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
val isUserAdmin = room.isOwnUserAdmin()
-
+ val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } }
val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.displayName).trim() } }
val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } }
val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } }
- val isPublic by remember { derivedStateOf { roomInfo?.isPublic.orFalse() } }
+ val joinRule by remember { derivedStateOf { roomInfo?.joinRule } }
val canShowPinnedMessages = isPinnedMessagesFeatureEnabled()
+ var canShowMediaGallery by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ canShowMediaGallery = featureFlagService.isFeatureEnabled(FeatureFlags.MediaGallery)
+ }
val pinnedMessagesCount by remember { derivedStateOf { roomInfo?.pinnedEventIds?.size } }
LaunchedEffect(Unit) {
@@ -92,6 +99,7 @@ class RoomDetailsPresenter @Inject constructor(
val membersState by room.membersStateFlow.collectAsState()
val canInvite by getCanInvite(membersState)
+
val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME)
val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR)
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
@@ -103,7 +111,6 @@ class RoomDetailsPresenter @Inject constructor(
val topicState = remember(canEditTopic, roomTopic, roomType) {
val topic = roomTopic
-
when {
!topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic)
canEditTopic && roomType is RoomDetailsType.Room -> RoomTopicState.CanAddTopic
@@ -111,6 +118,15 @@ class RoomDetailsPresenter @Inject constructor(
}
}
+ val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value)
+ val isKnockRequestsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(false)
+ val knockRequestsCount by produceState(null) {
+ room.knockRequestsFlow.collect { value = it.size }
+ }
+ val canShowKnockRequests by remember {
+ derivedStateOf { isKnockRequestsEnabled && canHandleKnockRequests && joinRule == JoinRule.Knock }
+ }
+
val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState()
fun handleEvents(event: RoomDetailsEvent) {
@@ -152,10 +168,13 @@ class RoomDetailsPresenter @Inject constructor(
roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(),
isFavorite = isFavorite,
displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin,
- isPublic = isPublic,
+ isPublic = joinRule == JoinRule.Public,
heroes = roomInfo?.heroes.orEmpty().toPersistentList(),
canShowPinnedMessages = canShowPinnedMessages,
+ canShowMediaGallery = canShowMediaGallery,
pinnedMessagesCount = pinnedMessagesCount,
+ canShowKnockRequests = canShowKnockRequests,
+ knockRequestsCount = knockRequestsCount,
eventSink = ::handleEvents,
)
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
index acb55a824c3..81ed06f5d1c 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
@@ -41,7 +41,10 @@ data class RoomDetailsState(
val isPublic: Boolean,
val heroes: ImmutableList,
val canShowPinnedMessages: Boolean,
+ val canShowMediaGallery: Boolean,
val pinnedMessagesCount: Int?,
+ val canShowKnockRequests: Boolean,
+ val knockRequestsCount: Int?,
val eventSink: (RoomDetailsEvent) -> Unit
) {
val roomBadges = buildList {
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
index cea460030ea..10ac6b4727c 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
@@ -101,7 +101,10 @@ fun aRoomDetailsState(
isPublic: Boolean = true,
heroes: List = emptyList(),
canShowPinnedMessages: Boolean = true,
+ canShowMediaGallery: Boolean = true,
pinnedMessagesCount: Int? = null,
+ canShowKnockRequests: Boolean = false,
+ knockRequestsCount: Int? = null,
eventSink: (RoomDetailsEvent) -> Unit = {},
) = RoomDetailsState(
isDebugBuild = false,
@@ -125,7 +128,10 @@ fun aRoomDetailsState(
isPublic = isPublic,
heroes = heroes.toPersistentList(),
canShowPinnedMessages = canShowPinnedMessages,
+ canShowMediaGallery = canShowMediaGallery,
pinnedMessagesCount = pinnedMessagesCount,
+ canShowKnockRequests = canShowKnockRequests,
+ knockRequestsCount = knockRequestsCount,
eventSink = eventSink
)
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 5a99377b4a5..d34c3480e9c 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
@@ -101,9 +101,11 @@ fun RoomDetailsView(
invitePeople: () -> Unit,
openAvatarPreview: (name: String, url: String) -> Unit,
openPollHistory: () -> Unit,
+ openMediaGallery: () -> Unit,
openAdminSettings: () -> Unit,
onJoinCallClick: () -> Unit,
onPinnedMessagesClick: () -> Unit,
+ onKnockRequestsClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
@@ -207,13 +209,23 @@ fun RoomDetailsView(
memberCount = state.memberCount,
openRoomMemberList = openRoomMemberList,
)
+ if (state.canShowKnockRequests) {
+ KnockRequestsItem(
+ knockRequestsCount = state.knockRequestsCount,
+ onKnockRequestsClick = onKnockRequestsClick
+ )
+ }
}
}
PollsSection(
openPollHistory = openPollHistory
)
-
+ if (state.canShowMediaGallery) {
+ MediaGallerySection(
+ onClick = openMediaGallery
+ )
+ }
if (state.isEncrypted) {
SecuritySection()
}
@@ -232,6 +244,20 @@ fun RoomDetailsView(
}
}
+@Composable
+private fun KnockRequestsItem(knockRequestsCount: Int?, onKnockRequestsClick: () -> Unit) {
+ ListItem(
+ headlineContent = { Text(stringResource(R.string.screen_room_details_requests_to_join_title)) },
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.AskToJoin())),
+ trailingContent = if (knockRequestsCount == null || knockRequestsCount == 0) {
+ null
+ } else {
+ ListItemContent.Text(knockRequestsCount.toString())
+ },
+ onClick = onKnockRequestsClick,
+ )
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RoomDetailsTopBar(
@@ -528,7 +554,7 @@ private fun PinnedMessagesItem(
) {
val analyticsService = LocalAnalyticsService.current
ListItem(
- headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) },
+ headlineContent = { Text(stringResource(R.string.screen_room_details_pinned_events_row_title)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())),
trailingContent =
if (pinnedMessagesCount == null) {
@@ -558,6 +584,19 @@ private fun PollsSection(
}
}
+@Composable
+private fun MediaGallerySection(
+ onClick: () -> Unit,
+) {
+ PreferenceCategory {
+ ListItem(
+ headlineContent = { Text(stringResource(R.string.screen_room_details_media_gallery_title)) },
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Image())),
+ onClick = onClick,
+ )
+ }
+}
+
@Composable
private fun SecuritySection() {
PreferenceCategory(title = stringResource(R.string.screen_room_details_security_title)) {
@@ -613,8 +652,10 @@ private fun ContentToPreview(state: RoomDetailsState) {
invitePeople = {},
openAvatarPreview = { _, _ -> },
openPollHistory = {},
+ openMediaGallery = {},
openAdminSettings = {},
onJoinCallClick = {},
onPinnedMessagesClick = {},
+ onKnockRequestsClick = {},
)
}
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 c1e4369aaad..deadcb03104 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
@@ -12,6 +12,7 @@ import dagger.Module
import dagger.Provides
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
+import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
@@ -21,6 +22,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
object RoomMemberModule {
@Provides
fun provideRoomMemberDetailsPresenterFactory(
+ buildMeta: BuildMeta,
room: MatrixRoom,
userProfilePresenterFactory: UserProfilePresenterFactory,
): RoomMemberDetailsPresenter.Factory {
@@ -28,6 +30,7 @@ object RoomMemberModule {
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
return RoomMemberDetailsPresenter(
roomMemberId = roomMemberId,
+ buildMeta = buildMeta,
room = room,
userProfilePresenterFactory = userProfilePresenterFactory,
)
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 06748b09995..60aa00150bd 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
@@ -8,7 +8,6 @@
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
@@ -21,7 +20,6 @@ import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.userprofile.shared.UserProfileView
-import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
@@ -73,12 +71,6 @@ class RoomMemberDetailsNode @AssistedInject constructor(
val state = presenter.present()
- LaunchedEffect(state.startDmActionState) {
- val result = state.startDmActionState
- if (result is AsyncAction.Success) {
- onStartDM(result.data)
- }
- }
UserProfileView(
state = state,
modifier = modifier,
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 0e6d9052f09..81990b7cb40 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
@@ -16,6 +16,7 @@ import dagger.assisted.AssistedInject
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
import io.element.android.features.userprofile.api.UserProfileState
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.meta.BuildMeta
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
@@ -26,6 +27,7 @@ import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
*/
class RoomMemberDetailsPresenter @AssistedInject constructor(
@Assisted private val roomMemberId: UserId,
+ private val buildMeta: BuildMeta,
private val room: MatrixRoom,
userProfilePresenterFactory: UserProfilePresenterFactory,
) : Presenter {
@@ -61,6 +63,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
val userProfileState = userProfilePresenter.present()
return userProfileState.copy(
+ isDebugBuild = buildMeta.isDebuggable,
userName = roomUserName ?: userProfileState.userName,
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt
index 3b94c7223bb..7785b8f0229 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt
@@ -213,33 +213,32 @@ private fun RoomMemberActionsBottomSheet(
.padding(bottom = 28.dp)
.align(Alignment.CenterHorizontally)
)
- // TCHAP display a value generated from userId if displayname does not exist
- roomMember.getBestName().let {
+ Text(
+ // TCHAP display a value generated from userId if displayname does not exist
+ text = roomMember.getBestName(),
+ style = ElementTheme.typography.fontHeadingLgBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ .fillMaxWidth()
+ )
+ // TCHAP hide the Matrix Id in release mode
+ if (isDebugBuild) {
Text(
- text = it,
- style = ElementTheme.typography.fontHeadingLgBold,
+ text = roomMember.userId.toString(),
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
modifier = Modifier
- .padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
+ .padding(horizontal = 16.dp)
.fillMaxWidth()
)
- if (isDebugBuild) { // TCHAP hide the Matrix Id in release mode
- Text(
- text = roomMember.userId.toString(),
- style = ElementTheme.typography.fontBodyLgRegular,
- color = ElementTheme.colors.textSecondary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- textAlign = TextAlign.Center,
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .fillMaxWidth()
- )
- }
- Spacer(modifier = Modifier.height(32.dp))
}
+ Spacer(modifier = Modifier.height(32.dp))
for (action in actions) {
when (action) {
diff --git a/features/roomdetails/impl/src/main/res/values-be/translations.xml b/features/roomdetails/impl/src/main/res/values-be/translations.xml
index 7cbf1fcd6df..6ba7e59a2ed 100644
--- a/features/roomdetails/impl/src/main/res/values-be/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-be/translations.xml
@@ -52,6 +52,7 @@
"Уласныя"
"Стандартныя"
"Апавяшчэнні"
+ "Замацаваныя паведамленні"
"Ролі і дазволы"
"Назва пакоя"
"Бяспека"
diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml
index 09003dc6108..2946530857c 100644
--- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml
@@ -49,11 +49,15 @@
"Pozvat přátele"
"Opustit konverzaci"
"Opustit místnost"
+ "Média a soubory"
"Vlastní"
"Výchozí"
"Oznámení"
+ "Připnuté zprávy"
+ "Žádosti o vstup"
"Role a oprávnění"
"Název místnosti"
+ "Zabezpečení a soukromí"
"Zabezpečení"
"Sdílet místnost"
"Informace o místnosti"
diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml
index f3bb984173f..643ed52d98a 100644
--- a/features/roomdetails/impl/src/main/res/values-de/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml
@@ -49,9 +49,12 @@
"Personen einladen"
"Unterhaltung verlassen"
"Verlassen"
+ "Medien und Dateien"
"Benutzerdefiniert"
"Standard"
"Benachrichtigungen"
+ "Fixierte Nachrichten"
+ "Beitrittsanfragen"
"Rollen und Berechtigungen"
"Raumname"
"Sicherheit"
diff --git a/features/roomdetails/impl/src/main/res/values-el/translations.xml b/features/roomdetails/impl/src/main/res/values-el/translations.xml
index 57874ff30be..88d651a83a6 100644
--- a/features/roomdetails/impl/src/main/res/values-el/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-el/translations.xml
@@ -49,11 +49,15 @@
"Πρόσκληση ατόμων"
"Αποχώρηση από τη συζήτηση"
"Αποχώρηση από το δωμάτιο"
+ "Πολυμέσα και αρχεία"
"Προσαρμοσμένο"
"Προεπιλογή"
"Ειδοποιήσεις"
+ "Καρφιτσωμένα μηνύματα"
+ "Αιτήματα συμμετοχής"
"Ρόλοι και δικαιώματα"
"Όνομα δωματίου"
+ "Ασφάλεια & απόρρητο"
"Ασφάλεια"
"Κοινή χρήση δωματίου"
"Πληροφορίες δωματίου"
diff --git a/features/roomdetails/impl/src/main/res/values-et/translations.xml b/features/roomdetails/impl/src/main/res/values-et/translations.xml
index a8c197ce7d1..7ff262314f2 100644
--- a/features/roomdetails/impl/src/main/res/values-et/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-et/translations.xml
@@ -49,9 +49,12 @@
"Kutsu osalejaid"
"Lahku vestlusest"
"Lahku jututoast"
+ "Meedia ja failid"
"Kohandatud"
"Vaikimisi"
"Teavitused"
+ "Esiletõstetud sõnumid"
+ "Liitumispalved"
"Rollid ja õigused"
"Jututoa nimi"
"Turvalisus"
diff --git a/features/roomdetails/impl/src/main/res/values-fa/translations.xml b/features/roomdetails/impl/src/main/res/values-fa/translations.xml
index ca25c834612..491435d52de 100644
--- a/features/roomdetails/impl/src/main/res/values-fa/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-fa/translations.xml
@@ -48,6 +48,7 @@
"سفارشی"
"پیشگزیده"
"آگاهیها"
+ "پیامهای سنجاق شده"
"نقشها و اجازهها"
"نام اتاق"
"امنیت"
diff --git a/features/roomdetails/impl/src/main/res/values-fi/translations.xml b/features/roomdetails/impl/src/main/res/values-fi/translations.xml
index db8b78d7790..7aa3a105e1a 100644
--- a/features/roomdetails/impl/src/main/res/values-fi/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-fi/translations.xml
@@ -49,9 +49,12 @@
"Kutsu ihmisiä"
"Poistu keskustelusta"
"Poistu huoneesta"
+ "Media ja tiedostot"
"Mukautettu"
"Oletus"
"Ilmoitukset"
+ "Kiinnitetyt viestit"
+ "Liittymispyynnöt"
"Roolit ja oikeudet"
"Huoneen nimi"
"Turvallisuus"
@@ -79,7 +82,7 @@
"Näytä profiili"
"Porttikiellot"
"Jäsenet"
- "Kutsutut"
+ "Kutsuttu"
"Poistetaan käyttäjää %1$s huoneesta…"
"Ylläpitäjä"
"Valvoja"
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 5766d1e0f12..2e28e6e1108 100644
--- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml
@@ -31,7 +31,7 @@
"Modérateurs"
"Membres"
"Vous avez des modifications non-enregistrées."
- "Enregistrer les changements?"
+ "Enregistrer les changements ?"
"Ajouter un sujet"
"Déjà membre"
"Déjà invité(e)"
@@ -49,11 +49,15 @@
"Inviter des amis"
"Quitter la discussion"
"Quitter le salon"
+ "Médias et fichiers"
"Personnalisé"
"Défaut"
"Notifications"
+ "Messages épinglés"
+ "Demandes en attente"
"Rôles et autorisations"
"Nom du salon"
+ "Sécurité & confidentialité"
"Sécurité"
"Partager le salon"
"Informations du salon"
@@ -61,7 +65,7 @@
"Mise à jour du salon…"
"Bannir"
"L‘utilisateur ne pourra pas rejoindre le salon à nouveau, même si il est invité."
- "Êtes-vous certain de vouloir bannir ce membre?"
+ "Êtes-vous certain de vouloir bannir ce membre ?"
"Il n’y a pas d’utilisateur banni dans ce salon."
"Bannissement de %1$s"
@@ -109,7 +113,7 @@
"Autorisations"
"Réinitialisation des autorisations"
"La réinitialisation des autorisations entraîne la perte des réglages actuels."
- "Réinitialisation des autorisations?"
+ "Réinitialisation des autorisations ?"
"Rôles"
"Détails du salon"
"Rôles et autorisations"
diff --git a/features/roomdetails/impl/src/main/res/values-hu/translations.xml b/features/roomdetails/impl/src/main/res/values-hu/translations.xml
index 8f992b1c7d7..be27d0fb321 100644
--- a/features/roomdetails/impl/src/main/res/values-hu/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-hu/translations.xml
@@ -49,11 +49,15 @@
"Ismerősök meghívása"
"Beszélgetés elhagyása"
"Szoba elhagyása"
+ "Média és fájlok"
"Egyéni"
"Alapértelmezett"
"Értesítések"
+ "Kitűzött üzenetek"
+ "Csatlakozási kérelem"
"Szerepkörök és jogosultságok"
"Szoba neve"
+ "Biztonság és adatvédelem"
"Biztonság"
"Szoba megosztása"
"Szobainformációk"
diff --git a/features/roomdetails/impl/src/main/res/values-in/translations.xml b/features/roomdetails/impl/src/main/res/values-in/translations.xml
index cd3ba931462..730adaa2931 100644
--- a/features/roomdetails/impl/src/main/res/values-in/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-in/translations.xml
@@ -52,6 +52,7 @@
"Khusus"
"Bawaan"
"Notifikasi"
+ "Pesan yang disematkan"
"Peran dan perizinan"
"Nama ruangan"
"Keamanan"
diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml
index fe752f28749..eb48791320f 100644
--- a/features/roomdetails/impl/src/main/res/values-it/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml
@@ -52,6 +52,8 @@
"Personalizzato"
"Predefinito"
"Notifiche"
+ "Messaggi fissati"
+ "Richieste di accesso"
"Ruoli e autorizzazioni"
"Nome stanza"
"Sicurezza"
diff --git a/features/roomdetails/impl/src/main/res/values-nl/translations.xml b/features/roomdetails/impl/src/main/res/values-nl/translations.xml
index a3a4dc965a9..6c677ea68ee 100644
--- a/features/roomdetails/impl/src/main/res/values-nl/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-nl/translations.xml
@@ -52,6 +52,7 @@
"Aangepast"
"Standaard"
"Meldingen"
+ "Vastgezette berichten"
"Rollen en rechten"
"Naam van de kamer"
"Beveiliging"
diff --git a/features/roomdetails/impl/src/main/res/values-pl/translations.xml b/features/roomdetails/impl/src/main/res/values-pl/translations.xml
index 80195a14ea8..aabbadf3feb 100644
--- a/features/roomdetails/impl/src/main/res/values-pl/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-pl/translations.xml
@@ -52,6 +52,7 @@
"Niestandardowy"
"Domyślny"
"Powiadomienia"
+ "Przypięte wiadomości"
"Role i uprawnienia"
"Nazwa pokoju"
"Bezpieczeństwo"
diff --git a/features/roomdetails/impl/src/main/res/values-pt/translations.xml b/features/roomdetails/impl/src/main/res/values-pt/translations.xml
index 5dde97ec98c..294c0e4a76f 100644
--- a/features/roomdetails/impl/src/main/res/values-pt/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-pt/translations.xml
@@ -52,6 +52,7 @@
"Personalizado"
"Predefinição"
"Notificações"
+ "Mensagens afixadas"
"Cargos e permissões"
"Nome da sala"
"Segurança"
diff --git a/features/roomdetails/impl/src/main/res/values-ru/translations.xml b/features/roomdetails/impl/src/main/res/values-ru/translations.xml
index 0ec4c6fd801..42e8fb0562a 100644
--- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml
@@ -49,9 +49,12 @@
"Пригласить в комнату"
"Покинуть беседу"
"Покинуть комнату"
+ "Медиа и файлы"
"Пользовательский"
"По умолчанию"
"Уведомления"
+ "Закрепленные сообщения"
+ "Запросы на вступление"
"Роли и разрешения"
"Название комнаты"
"Безопасность"
diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml
index c905333c2c8..4b2d289b356 100644
--- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml
@@ -52,6 +52,8 @@
"Vlastné"
"Predvolené"
"Oznámenia"
+ "Pripnuté správy"
+ "Žiadosti o vstup"
"Roly a povolenia"
"Názov miestnosti"
"Bezpečnosť"
diff --git a/features/roomdetails/impl/src/main/res/values-sv/translations.xml b/features/roomdetails/impl/src/main/res/values-sv/translations.xml
index eed1cfcbb10..7f20d978d47 100644
--- a/features/roomdetails/impl/src/main/res/values-sv/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-sv/translations.xml
@@ -52,6 +52,7 @@
"Anpassad"
"Förval"
"Aviseringar"
+ "Fästa meddelanden"
"Roller och behörigheter"
"Rumsnamn"
"Säkerhet"
diff --git a/features/roomdetails/impl/src/main/res/values-uk/translations.xml b/features/roomdetails/impl/src/main/res/values-uk/translations.xml
index 616f2935540..49a7f147cf4 100644
--- a/features/roomdetails/impl/src/main/res/values-uk/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-uk/translations.xml
@@ -52,6 +52,7 @@
"Власні"
"Типово"
"Сповіщення"
+ "Закріплені повідомлення"
"Ролі та дозволи"
"Назва кімнати"
"Безпека"
diff --git a/features/roomdetails/impl/src/main/res/values-zh/translations.xml b/features/roomdetails/impl/src/main/res/values-zh/translations.xml
index a2f6e26e9ba..1198de917c7 100644
--- a/features/roomdetails/impl/src/main/res/values-zh/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-zh/translations.xml
@@ -52,6 +52,7 @@
"自定义"
"默认"
"通知"
+ "置顶消息"
"角色与权限"
"聊天室名称"
"安全"
diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml
index 19400c85b37..bd602d3ed32 100644
--- a/features/roomdetails/impl/src/main/res/values/localazy.xml
+++ b/features/roomdetails/impl/src/main/res/values/localazy.xml
@@ -49,11 +49,15 @@
"Invite people"
"Leave conversation"
"Leave room"
+ "Media and files"
"Custom"
"Default"
"Notifications"
+ "Pinned messages"
+ "Requests to join"
"Roles and permissions"
"Room name"
+ "Security & privacy"
"Security"
"Share room"
"Room info"
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
index a0e1c1a6777..4d5cffb84cf 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenterTest.kt
@@ -84,6 +84,7 @@ class RoomDetailsPresenterTest {
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
return RoomMemberDetailsPresenter(
roomMemberId = roomMemberId,
+ buildMeta = buildMeta,
room = room,
userProfilePresenterFactory = {
Presenter { aUserProfileState() }
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt
index e4eb8d5f691..11858929e3d 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt
@@ -79,6 +79,17 @@ class RoomDetailsViewTest {
}
}
+ @Config(qualifiers = "h1024dp")
+ @Test
+ fun `click on media gallery invokes expected callback`() {
+ ensureCalledOnce { callback ->
+ rule.setRoomDetailView(
+ openMediaGallery = callback,
+ )
+ rule.clickOn(R.string.screen_room_details_media_gallery_title)
+ }
+ }
+
@Config(qualifiers = "h1024dp")
@Test
fun `click on notification invokes expected callback`() {
@@ -129,7 +140,7 @@ class RoomDetailsViewTest {
),
onPinnedMessagesClick = callback,
)
- rule.clickOn(CommonStrings.screen_room_details_pinned_events_row_title)
+ rule.clickOn(R.string.screen_room_details_pinned_events_row_title)
}
}
@@ -241,7 +252,7 @@ class RoomDetailsViewTest {
eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true))
}
- @Config(qualifiers = "h1024dp")
+ @Config(qualifiers = "h1500dp")
@Test
fun `click on leave emit expected Event`() {
val eventsRecorder = EventsRecorder()
@@ -253,6 +264,21 @@ class RoomDetailsViewTest {
rule.clickOn(R.string.screen_room_details_leave_room_title)
eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom)
}
+
+ @Config(qualifiers = "h1024dp")
+ @Test
+ fun `click on knock requests invokes expected callback`() {
+ ensureCalledOnce { callback ->
+ rule.setRoomDetailView(
+ state = aRoomDetailsState(
+ eventSink = EventsRecorder(expectEvents = false),
+ canShowKnockRequests = true,
+ ),
+ onKnockRequestsClick = callback,
+ )
+ rule.clickOn(R.string.screen_room_details_requests_to_join_title)
+ }
+ }
}
private fun AndroidComposeTestRule.setRoomDetailView(
@@ -267,9 +293,11 @@ private fun AndroidComposeTestRule.setRoomD
invitePeople: () -> Unit = EnsureNeverCalled(),
openAvatarPreview: (name: String, url: String) -> Unit = EnsureNeverCalledWithTwoParams(),
openPollHistory: () -> Unit = EnsureNeverCalled(),
+ openMediaGallery: () -> Unit = EnsureNeverCalled(),
openAdminSettings: () -> Unit = EnsureNeverCalled(),
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
+ onKnockRequestsClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
RoomDetailsView(
@@ -282,9 +310,11 @@ private fun AndroidComposeTestRule.setRoomD
invitePeople = invitePeople,
openAvatarPreview = openAvatarPreview,
openPollHistory = openPollHistory,
+ openMediaGallery = openMediaGallery,
openAdminSettings = openAdminSettings,
onJoinCallClick = onJoinCallClick,
onPinnedMessagesClick = onPinnedMessagesClick,
+ onKnockRequestsClick = onKnockRequestsClick,
)
}
}
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt
index 1ea63f6ccea..b4f0e28b1b9 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTest.kt
@@ -17,10 +17,12 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe
import io.element.android.features.userprofile.api.UserProfilePresenterFactory
import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.test.AN_EXCEPTION
+import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -158,6 +160,7 @@ class RoomMemberDetailsPresenterTest {
}
private fun createRoomMemberDetailsPresenter(
+ buildMeta: BuildMeta = aBuildMeta(),
room: MatrixRoom,
userProfilePresenterFactory: UserProfilePresenterFactory = UserProfilePresenterFactory {
Presenter {
@@ -170,6 +173,7 @@ class RoomMemberDetailsPresenterTest {
): RoomMemberDetailsPresenter {
return RoomMemberDetailsPresenter(
roomMemberId = UserId("@alice:server.org"),
+ buildMeta = buildMeta,
room = room,
userProfilePresenterFactory = userProfilePresenterFactory
)
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt
index 6ba1d8ed8ea..f44f192e775 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListContextMenu.kt
@@ -61,6 +61,10 @@ fun RoomListContextMenu(
onFavoriteChange = { isFavorite ->
eventSink(RoomListEvents.SetRoomIsFavorite(contextMenu.roomId, isFavorite))
},
+ onClearCacheRoomClick = {
+ eventSink(RoomListEvents.HideContextMenu)
+ eventSink(RoomListEvents.ClearCacheOfRoom(contextMenu.roomId))
+ },
)
}
}
@@ -73,6 +77,7 @@ private fun RoomListModalBottomSheetContent(
onFavoriteChange: (isFavorite: Boolean) -> Unit,
onRoomMarkReadClick: () -> Unit,
onRoomMarkUnreadClick: () -> Unit,
+ onClearCacheRoomClick: () -> Unit,
) {
Column(
modifier = Modifier.fillMaxWidth()
@@ -177,6 +182,18 @@ private fun RoomListModalBottomSheetContent(
),
style = ListItemStyle.Destructive,
)
+ if (contextMenu.eventCacheFeatureFlagEnabled) {
+ ListItem(
+ headlineContent = {
+ Text(text = "Clear cache for this room")
+ },
+ modifier = Modifier.clickable { onClearCacheRoomClick() },
+ leadingContent = ListItemContent.Icon(
+ iconSource = IconSource.Vector(CompoundIcons.Delete())
+ ),
+ style = ListItemStyle.Primary,
+ )
+ }
}
}
@@ -195,5 +212,6 @@ internal fun RoomListModalBottomSheetContentPreview(
onRoomSettingsClick = {},
onLeaveRoomClick = {},
onFavoriteChange = {},
+ onClearCacheRoomClick = {},
)
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
index 9a8f2353ba4..67c4544aaa8 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListEvents.kt
@@ -25,4 +25,5 @@ sealed interface RoomListEvents {
data class MarkAsRead(val roomId: RoomId) : ContextMenuEvents
data class MarkAsUnread(val roomId: RoomId) : ContextMenuEvents
data class SetRoomIsFavorite(val roomId: RoomId, val isFavorite: Boolean) : ContextMenuEvents
+ data class ClearCacheOfRoom(val roomId: RoomId) : ContextMenuEvents
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
index c346e27a1cc..60d5511f9ec 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
@@ -148,6 +148,7 @@ class RoomListPresenter @Inject constructor(
AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData())
)
}
+ is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId)
}
}
@@ -258,7 +259,8 @@ class RoomListPresenter @Inject constructor(
isDm = event.roomListRoomSummary.isDm,
isFavorite = event.roomListRoomSummary.isFavorite,
markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread),
- hasNewContent = event.roomListRoomSummary.hasNewContent
+ hasNewContent = event.roomListRoomSummary.hasNewContent,
+ eventCacheFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.EventCache),
)
contextMenuState.value = initialState
@@ -315,6 +317,12 @@ class RoomListPresenter @Inject constructor(
}
}
+ private fun CoroutineScope.clearCacheOfRoom(roomId: RoomId) = launch {
+ client.getRoom(roomId)?.use { room ->
+ room.clearEventCacheStorage()
+ }
+ }
+
/**
* Checks if the user needs to migrate to a native sliding sync version.
*/
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
index 0bfc6e665f4..fafe0c9cdde 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt
@@ -46,6 +46,7 @@ data class RoomListState(
val isDm: Boolean,
val isFavorite: Boolean,
val markAsUnreadFeatureFlagEnabled: Boolean,
+ val eventCacheFeatureFlagEnabled: Boolean,
val hasNewContent: Boolean,
) : ContextMenu
}
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateContextMenuShownProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateContextMenuShownProvider.kt
index 6b74c7934a9..36b951f73c3 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateContextMenuShownProvider.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateContextMenuShownProvider.kt
@@ -31,4 +31,5 @@ internal fun aContextMenuShown(
markAsUnreadFeatureFlagEnabled = true,
hasNewContent = hasNewContent,
isFavorite = isFavorite,
+ eventCacheFeatureFlagEnabled = false,
)
diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt
index 534de3c4d4a..a77f1545da5 100644
--- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt
+++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt
@@ -10,7 +10,8 @@ package io.element.android.features.roomlist.impl.datasource
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.core.extensions.orEmpty
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
@@ -22,7 +23,7 @@ import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
class RoomListRoomSummaryFactory @Inject constructor(
- private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
+ private val dateFormatter: DateFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter,
) {
fun create(roomSummary: RoomSummary): RoomListRoomSummary {
@@ -36,7 +37,11 @@ class RoomListRoomSummaryFactory @Inject constructor(
numberOfUnreadMentions = roomInfo.numUnreadMentions,
numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
isMarkedUnread = roomInfo.isMarkedUnread,
- timestamp = lastMessageTimestampFormatter.format(roomSummary.lastMessageTimestamp),
+ timestamp = dateFormatter.format(
+ timestamp = roomSummary.lastMessageTimestamp,
+ mode = DateFormatterMode.TimeOrDate,
+ useRelative = true,
+ ),
lastMessage = roomSummary.lastMessage?.let { message ->
roomLastMessageFormatter.format(message.event, roomInfo.isDm)
}.orEmpty(),
diff --git a/features/roomlist/impl/src/main/res/values-fr/translations.xml b/features/roomlist/impl/src/main/res/values-fr/translations.xml
index 315efb34787..04fb371eebc 100644
--- a/features/roomlist/impl/src/main/res/values-fr/translations.xml
+++ b/features/roomlist/impl/src/main/res/values-fr/translations.xml
@@ -9,7 +9,7 @@
"Configurer la récupération"
"Confirmez votre clé de récupération pour conserver l’accès à votre stockage de clés et à l’historique des messages."
"Saisissez votre clé de récupération"
- "Clé de récupération oubliée?"
+ "Clé de récupération oubliée ?"
"Le stockage de vos clés n’est pas synchronisé"
"Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé."
"Améliorez votre expérience d’appel"
@@ -39,8 +39,8 @@ En attendant, vous pouvez désélectionner des filtres pour voir vos autres salo
"Salons"
"Vous n’êtes membre d’aucun salon"
"Non-lus"
- "Félicitations!
-Vous n’avez plus de messages non-lus!"
+ "Félicitations !
+Vous n’avez plus de messages non-lus !"
"Conversations"
"Marquer comme lu"
"Marquer comme non lu"
diff --git a/features/roomlist/impl/src/main/res/values-it/translations.xml b/features/roomlist/impl/src/main/res/values-it/translations.xml
index af305d4b7d5..cd248d73722 100644
--- a/features/roomlist/impl/src/main/res/values-it/translations.xml
+++ b/features/roomlist/impl/src/main/res/values-it/translations.xml
@@ -7,8 +7,10 @@
"Genera una nuova chiave di recupero che può essere usata per ripristinare la cronologia dei messaggi crittografati nel caso in cui tu perda l\'accesso ai tuoi dispositivi."
"Configura il recupero"
"Configura il ripristino"
- "Il backup della chat non è attualmente sincronizzato. Devi confermare la chiave di recupero per mantenere l\'accesso al backup della chat."
- "Inserisci la chiave di recupero"
+ "Conferma la chiave di recupero per mantenere l\'accesso all\'archiviazione delle chiavi e alla cronologia dei messaggi."
+ "Inserisci la tua chiave di recupero"
+ "Hai dimenticato la chiave di recupero?"
+ "L\'archiviazione delle chiavi non è sincronizzata"
"Per non perdere mai una chiamata importante, modifica le impostazioni per consentire le notifiche a schermo intero quando il telefono è bloccato."
"Migliora la tua esperienza di chiamata"
"Vuoi davvero rifiutare l\'invito ad entrare in %1$s?"
@@ -17,6 +19,7 @@
"Rifiuta l\'invito alla conversazione"
"Nessun invito"
"%1$s (%2$s) ti ha invitato"
+ "Richiesta di accesso inviata"
"Si tratta di una procedura che si effettua una sola volta, grazie per l\'attesa."
"Configurazione del tuo account."
"Crea una nuova conversazione o stanza"
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
index f71a8434b6d..9e334cf382c 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
@@ -32,9 +32,8 @@ import io.element.android.features.roomlist.impl.search.aRoomListSearchState
import io.element.android.libraries.androidutils.system.DateTimeObserver
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
-import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
-import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
@@ -190,6 +189,7 @@ class RoomListPresenterTest {
createRoomListRoomSummary(
numberOfUnreadMentions = 1,
numberOfUnreadMessages = 2,
+ timestamp = "0 TimeOrDate true",
)
)
cancelAndIgnoreRemainingEvents()
@@ -290,6 +290,7 @@ class RoomListPresenterTest {
isDm = false,
isFavorite = false,
markAsUnreadFeatureFlagEnabled = true,
+ eventCacheFeatureFlagEnabled = false,
hasNewContent = false,
)
)
@@ -307,6 +308,7 @@ class RoomListPresenterTest {
isDm = false,
isFavorite = true,
markAsUnreadFeatureFlagEnabled = true,
+ eventCacheFeatureFlagEnabled = false,
hasNewContent = false,
)
)
@@ -337,6 +339,7 @@ class RoomListPresenterTest {
isDm = false,
isFavorite = false,
markAsUnreadFeatureFlagEnabled = true,
+ eventCacheFeatureFlagEnabled = false,
hasNewContent = false,
)
)
@@ -636,9 +639,7 @@ class RoomListPresenterTest {
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
- lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply {
- givenFormat(A_FORMATTED_DATE)
- },
+ dateFormatter: DateFormatter = FakeDateFormatter(),
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
@@ -656,7 +657,7 @@ class RoomListPresenterTest {
roomListDataSource = RoomListDataSource(
roomListService = client.roomListService,
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
- lastMessageTimestampFormatter = lastMessageTimestampFormatter,
+ dateFormatter = dateFormatter,
roomLastMessageFormatter = roomLastMessageFormatter,
),
coroutineDispatchers = testCoroutineDispatchers(),
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt
index f02c53e6f62..1839b35688e 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt
@@ -11,7 +11,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomlist.impl.FakeDateTimeObserver
import io.element.android.libraries.androidutils.system.DateTimeObserver
-import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.aRoomSummary
@@ -30,12 +30,12 @@ class RoomListDataSourceTest {
postAllRooms(listOf(aRoomSummary()))
}
val dateTimeObserver = FakeDateTimeObserver()
- val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter()
- lastMessageTimestampFormatter.givenFormat("Today")
+ var dateFormatterResult = "Today"
+ val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
val roomListDataSource = createRoomListDataSource(
roomListService = roomListService,
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
- lastMessageTimestampFormatter = lastMessageTimestampFormatter,
+ dateFormatter = dateFormatter,
),
dateTimeObserver = dateTimeObserver,
)
@@ -47,7 +47,7 @@ class RoomListDataSourceTest {
val initialRoomList = awaitItem()
assertThat(initialRoomList).isNotEmpty()
assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
- lastMessageTimestampFormatter.givenFormat("Yesterday")
+ dateFormatterResult = "Yesterday"
// Trigger a date change
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now()))
// Check there is a new list and it's not the same as the previous one
@@ -64,12 +64,12 @@ class RoomListDataSourceTest {
postAllRooms(listOf(aRoomSummary()))
}
val dateTimeObserver = FakeDateTimeObserver()
- val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter()
- lastMessageTimestampFormatter.givenFormat("Today")
+ var dateFormatterResult = "Today"
+ val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
val roomListDataSource = createRoomListDataSource(
roomListService = roomListService,
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
- lastMessageTimestampFormatter = lastMessageTimestampFormatter,
+ dateFormatter = dateFormatter,
),
dateTimeObserver = dateTimeObserver,
)
@@ -80,7 +80,7 @@ class RoomListDataSourceTest {
val initialRoomList = awaitItem()
assertThat(initialRoomList).isNotEmpty()
assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
- lastMessageTimestampFormatter.givenFormat("Yesterday")
+ dateFormatterResult = "Yesterday"
// Trigger a timezone change
dateTimeObserver.given(DateTimeObserver.Event.TimeZoneChanged)
// Check there is a new list and it's not the same as the previous one
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt
index 8a26120a9ee..41996b24db9 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt
@@ -7,13 +7,14 @@
package io.element.android.features.roomlist.impl.datasource
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
fun aRoomListRoomSummaryFactory(
- lastMessageTimestampFormatter: LastMessageTimestampFormatter = LastMessageTimestampFormatter { _ -> "Today" },
+ dateFormatter: DateFormatter = FakeDateFormatter { _, _, _ -> "Today" },
roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" }
) = RoomListRoomSummaryFactory(
- lastMessageTimestampFormatter = lastMessageTimestampFormatter,
+ dateFormatter = dateFormatter,
roomLastMessageFormatter = roomLastMessageFormatter
)
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt
index 7e91fa59de3..fbe7137ed89 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt
@@ -8,7 +8,6 @@
package io.element.android.features.roomlist.impl.model
import com.google.common.truth.Truth.assertThat
-import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@@ -84,6 +83,7 @@ internal fun createRoomListRoomSummary(
isFavorite: Boolean = false,
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
heroes: List = emptyList(),
+ timestamp: String? = null,
) = RoomListRoomSummary(
id = A_ROOM_ID.value,
roomId = A_ROOM_ID,
@@ -92,7 +92,7 @@ internal fun createRoomListRoomSummary(
numberOfUnreadMessages = numberOfUnreadMessages,
numberOfUnreadNotifications = numberOfUnreadNotifications,
isMarkedUnread = isMarkedUnread,
- timestamp = A_FORMATTED_DATE,
+ timestamp = timestamp,
lastMessage = "",
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
displayType = displayType,
diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt
index a28a15775a5..d75cd0d6e33 100644
--- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt
+++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt
@@ -13,7 +13,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory
import io.element.android.libraries.core.meta.BuildMeta
-import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
@@ -149,7 +149,7 @@ fun TestScope.createRoomListSearchPresenter(
dataSource = RoomListSearchDataSource(
roomListService = roomListService,
roomSummaryFactory = aRoomListRoomSummaryFactory(
- lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
+ dateFormatter = FakeDateFormatter(),
roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
),
coroutineDispatchers = testCoroutineDispatchers(),
diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
index 7d03eb7fbae..0cb87cb762d 100644
--- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
+++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt
@@ -9,6 +9,7 @@ package io.element.android.features.securebackup.impl.setup.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -24,8 +25,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillType
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
@@ -88,42 +91,38 @@ private fun RecoveryKeyStaticContent(
state: RecoveryKeyViewState,
onClick: (() -> Unit)?,
) {
- Row(
+ Box(
modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(14.dp))
- .background(
- color = ElementTheme.colors.bgSubtleSecondary,
- shape = RoundedCornerShape(14.dp)
- )
- .clickableIfNotNull(onClick)
- .padding(horizontal = 16.dp, vertical = 16.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .background(
+ color = ElementTheme.colors.bgSubtleSecondary,
+ )
+ .clickableIfNotNull(onClick)
+ .padding(horizontal = 16.dp, vertical = 11.dp),
+ contentAlignment = Alignment.Center,
) {
if (state.formattedRecoveryKey != null) {
- Text(
- text = state.formattedRecoveryKey,
- modifier = Modifier.weight(1f),
- )
- Icon(
- imageVector = CompoundIcons.Copy(),
- contentDescription = stringResource(id = CommonStrings.action_copy),
- tint = ElementTheme.colors.iconSecondary,
+ RecoveryKeyWithCopy(
+ recoveryKey = state.formattedRecoveryKey,
+ alpha = 1f,
)
} else {
+ // Use an invisible recovery key to ensure that the Box size is correct.
+ val fakeFormattedRecoveryKey = List(12) { "XXXX" }.joinToString(" ")
+ RecoveryKeyWithCopy(
+ recoveryKey = fakeFormattedRecoveryKey,
+ alpha = 0f,
+ )
Row(
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center,
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 11.dp)
) {
if (state.inProgress) {
CircularProgressIndicator(
modifier = Modifier
- .progressSemantics()
- .padding(end = 8.dp)
- .size(16.dp),
+ .progressSemantics()
+ .padding(end = 8.dp)
+ .size(16.dp),
color = ElementTheme.colors.textPrimary,
strokeWidth = 1.5.dp,
)
@@ -144,6 +143,31 @@ private fun RecoveryKeyStaticContent(
}
}
+@Composable
+private fun RecoveryKeyWithCopy(
+ recoveryKey: String,
+ alpha: Float,
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .alpha(alpha),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ text = recoveryKey,
+ color = ElementTheme.colors.textSecondary,
+ style = ElementTheme.typography.fontBodyLgRegular.copy(fontFamily = FontFamily.Monospace),
+ modifier = Modifier.weight(1f),
+ )
+ Icon(
+ imageVector = CompoundIcons.Copy(),
+ contentDescription = stringResource(id = CommonStrings.action_copy),
+ tint = ElementTheme.colors.iconSecondary,
+ )
+ }
+}
+
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun RecoveryKeyFormContent(
@@ -160,12 +184,12 @@ private fun RecoveryKeyFormContent(
}
TextField(
modifier = Modifier
- .fillMaxWidth()
- .testTag(TestTags.recoveryKey)
- .autofill(
- autofillTypes = listOf(AutofillType.Password),
- onFill = { onChange(it) },
- ),
+ .fillMaxWidth()
+ .testTag(TestTags.recoveryKey)
+ .autofill(
+ autofillTypes = listOf(AutofillType.Password),
+ onFill = { onChange(it) },
+ ),
minLines = 2,
value = state.formattedRecoveryKey.orEmpty(),
onValueChange = onChange,
diff --git a/features/securebackup/impl/src/main/res/values-fi/translations.xml b/features/securebackup/impl/src/main/res/values-fi/translations.xml
index 8d0c57cb7e0..7daf180c6bc 100644
--- a/features/securebackup/impl/src/main/res/values-fi/translations.xml
+++ b/features/securebackup/impl/src/main/res/values-fi/translations.xml
@@ -9,7 +9,7 @@
"Salli avainten säilytys"
"Vaihda palautusavain"
"Palauta kryptografinen identiteettisi ja viestihistoriasi palautusavaimella, jos olet menettänyt kaikki nykyiset laitteesi."
- "Käytä palautusavainta"
+ "Syötä palautusavain"
"Avainten säilytys ei ole tällä hetkellä synkronoitu."
"Ota palautus käyttöön"
"Pääset käsiksi salattuihin viesteihisi, jos menetät kaikki laitteesi tai olet kirjautunut ulos %1$s -sovelluksesta kaikkialla."
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 867d43b7e7b..3b3f2094fe7 100644
--- a/features/securebackup/impl/src/main/res/values-fr/translations.xml
+++ b/features/securebackup/impl/src/main/res/values-fr/translations.xml
@@ -12,7 +12,7 @@
"Utiliser la clé de récupération"
"Le stockage de vos clés est actuellement désynchronisé."
"Configurer la sauvegarde"
- "Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnectés de %1$s partout."
+ "Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnecté de %1$s partout."
"Ouvrez %1$s sur un ordinateur"
"Connectez-vous à nouveau à votre compte"
"Lorsque vous devrez vérifier la session, choisissez %1$s"
@@ -28,23 +28,23 @@
"Vous ne pouvez pas confirmer ? Vous devez réinitialiser votre identité."
"Désactiver"
"Vous perdrez vos messages chiffrés si vous vous déconnectez de toutes vos sessions."
- "Êtes-vous certain de vouloir désactiver la sauvegarde?"
- "Désactiver la sauvegarde supprimera votre clé de récupération actuelle et désactivera d’autres mesures de sécurité. Dans ce cas:"
+ "Êtes-vous certain de vouloir désactiver la sauvegarde ?"
+ "Désactiver la sauvegarde supprimera votre clé de récupération actuelle et désactivera d’autres mesures de sécurité. Dans ce cas :"
"Pas d’accès à l’historique des discussions chiffrées sur vos nouveaux appareils"
- "Perte de l’accès à vos messages chiffrés si vous êtes déconnectés de %1$s partout"
- "Êtes-vous certain de vouloir désactiver la sauvegarde?"
+ "Perte de l’accès à vos messages chiffrés si vous êtes déconnecté de %1$s partout"
+ "Êtes-vous certain de vouloir désactiver la sauvegarde ?"
"Obtenez une nouvelle clé de récupération dans le cas où vous avez oublié l’ancienne. Après le changement, l’ancienne clé ne sera plus utilisable."
"Générer une nouvelle clé"
"Ne partagez cela avec personne !"
"Clé de récupération modifée"
- "Changer la clé de récupération?"
+ "Changer la clé de récupération ?"
"Créer une nouvelle clé de récupération"
- "Assurez vous que personne d’autre ne regarde votre écran!"
+ "Assurez vous que personne d’autre ne regarde votre écran !"
"Veuillez réessayer pour confirmer l’accès à votre stockage de clés."
"Clé de récupération incorrecte"
"Si vous avez une clé de sécurité ou une phrase de sécurité, cela fonctionnera également."
"Saisissez la clé ici…"
- "Clé de récupération perdue?"
+ "Clé de récupération perdue ?"
"Clé de récupération confirmée"
"Saisissez votre clé de récupération"
"Clé de récupération copiée"
@@ -54,7 +54,7 @@
"Taper pour copier la clé"
"Sauvegarder la clé"
"La clé ne pourra plus être affichée après cette étape."
- "Avez-vous sauvegardé votre clé de récupération?"
+ "Avez-vous sauvegardé votre clé de récupération ?"
"Votre sauvegarde est protégée par votre clé de récupération. Si vous avez besoin d’une nouvelle clé après la configuration, vous pourrez en créer une nouvelle en cliquant sur \"Changer la clé de récupération\"."
"Générer la clé de récupération"
"Ne partagez cela avec personne !"
diff --git a/features/securebackup/impl/src/main/res/values-it/translations.xml b/features/securebackup/impl/src/main/res/values-it/translations.xml
index d6b629ea931..e9b8849a92e 100644
--- a/features/securebackup/impl/src/main/res/values-it/translations.xml
+++ b/features/securebackup/impl/src/main/res/values-it/translations.xml
@@ -2,11 +2,15 @@
"Disattiva il backup"
"Attiva il backup"
- "Il backup ti garantisce di non perdere la cronologia dei messaggi. %1$s."
- "Backup"
+ "Archivia la tua identità crittografica e le chiavi dei messaggi in modo sicuro sul server. Ciò ti consentirà di visualizzare la cronologia dei messaggi su tutti i nuovi dispositivi. %1$s."
+ "Archiviazione chiavi"
+ "L\'archiviazione delle chiavi deve essere attivata per configurare il ripristino."
+ "Carica le chiavi da questo dispositivo"
+ "Consenti l\'archiviazione delle chiavi"
"Cambia la chiave di recupero"
+ "Recupera la tua identità crittografica e la cronologia dei messaggi con una chiave di recupero se hai perso tutti i dispositivi esistenti."
"Inserisci la chiave di recupero"
- "Il backup delle conversazioni non è attualmente sincronizzato."
+ "L\'archiviazione delle chiavi non è sincronizzata."
"Configura il recupero"
"Ottieni l\'accesso ai tuoi messaggi cifrati se perdi tutti i tuoi dispositivi o se sei disconnesso da %1$s ovunque."
"Apri %1$s in un dispositivo desktop"
@@ -31,28 +35,29 @@
"Vuoi davvero disattivare il backup?"
"Ottieni una nuova chiave di recupero se hai perso quella esistente. Dopo averla cambiata, quella vecchia non funzionerà più."
"Genera una nuova chiave di recupero"
- "Assicurati di conservare la chiave di recupero in un posto sicuro"
+ "Non condividerla con nessuno!"
"Chiave di recupero cambiata"
"Cambiare la chiave di recupero?"
"Crea una nuova chiave di recupero"
"Assicurati che nessuno possa vedere questa schermata!"
- "Riprova per confermare l\'accesso al backup della chat."
+ "Riprova per confermare l\'accesso all\'archivio delle chiavi."
"Chiave di recupero errata"
"Se hai una chiave di sicurezza o una password, andrà bene anche questo."
"Inserisci…"
"Hai perso la chiave di recupero?"
"Chiave di recupero confermata"
+ "Inserisci la tua chiave di recupero"
"Chiave di recupero copiata"
"Generazione…"
"Salva la chiave di recupero"
- "Annota la chiave di recupero in un posto sicuro o salvala in un gestore di password."
+ "Annota questa chiave di recupero in un posto sicuro, come un gestore di password, una nota cifrata o una cassaforte fisica."
"Tocca per copiare la chiave di recupero"
"Salva la tua chiave di recupero"
"Dopo questo passaggio non potrai accedere alla nuova chiave di recupero."
"Hai salvato la chiave di recupero?"
"Il backup della chat è protetto da una chiave di recupero. Se hai bisogno di una nuova chiave di recupero dopo la configurazione, puoi ricrearla selezionando \"Cambia chiave di recupero\"."
"Genera la tua chiave di recupero"
- "Assicurati di conservare la chiave di recupero in un posto sicuro"
+ "Non condividerla con nessuno!"
"Configurazione del recupero completata"
"Configura il recupero"
"Sì, reimposta ora"
diff --git a/features/securebackup/impl/src/main/res/values-pt-rBR/translations.xml b/features/securebackup/impl/src/main/res/values-pt-rBR/translations.xml
index c78c7ad85ad..eed0e7f5764 100644
--- a/features/securebackup/impl/src/main/res/values-pt-rBR/translations.xml
+++ b/features/securebackup/impl/src/main/res/values-pt-rBR/translations.xml
@@ -2,11 +2,11 @@
"Desativar o backup"
"Ativar o backup"
- "O backup garante que você não perca seu histórico de mensagens. %1$s."
+ "Armazene sua identidade criptográfica e chaves de mensagem com segurança no servidor. Isso permitirá que você visualize seu histórico de mensagens em quaisquer novos dispositivos.%1$s ."
"Armazenamento de chaves"
"Alterar chave de recuperação"
"Insira a chave de recuperação"
- "Seu backup das conversas está atualmente fora de sincronia."
+ "Seu armazenamento de chaves está fora de sincronia no momento."
"Configurar a recuperação"
"Tenha acesso às suas mensagens criptografadas se você perder todos os seus dispositivos ou for desconectado do %1$s em qualquer lugar."
"Desligar"
@@ -18,7 +18,7 @@
"Tem certeza de que deseja desativar o backup?"
"Obtenha uma nova chave de recuperação caso tenha perdido a existente. Depois de alterar sua chave de recuperação, a antiga não funcionará mais."
"Gere uma nova chave de recuperação"
- "Certifique-se de que você pode armazenar sua chave de recuperação em algum lugar seguro"
+ "Não compartilhe isso com ninguém!"
"Chave de recuperação alterada"
"Alterar chave de recuperação?"
"Certifique-se de que ninguém possa ver essa tela!"
@@ -29,14 +29,14 @@
"Chave de recuperação copiada"
"Gerando…"
"Salvar chave de recuperação"
- "Anote sua chave de recuperação em algum lugar seguro ou salve-a em um gerenciador de senhas."
+ "Anote essa chave de recuperação em algum lugar seguro, como um gerenciador de senhas, uma nota criptografada ou um cofre físico."
"Toque para copiar a chave de recuperação"
"Salve sua chave de recuperação"
"Você não poderá acessar sua nova chave de recuperação após essa etapa."
"Você salvou sua chave de recuperação?"
"Seu backup das conversas é protegido por uma chave de recuperação. Se precisar de uma nova chave de recuperação após a configuração, você pode recriá-la selecionando “Alterar chave de recuperação”."
"Gere sua chave de recuperação"
- "Certifique-se de que você pode armazenar sua chave de recuperação em algum lugar seguro"
+ "Não compartilhe isso com ninguém!"
"Configuração de recuperação bem-sucedida"
"Configurar a recuperação"
"Inserir…"
diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt
index ce0d4a07f0a..d795f60715a 100644
--- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt
+++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt
@@ -30,6 +30,7 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
@@ -82,6 +83,10 @@ class UserProfileFlowNode @AssistedInject constructor(
override fun onDone() {
backstack.pop()
}
+
+ override fun onViewInTimeline(eventId: EventId) {
+ // Cannot happen
+ }
}
mediaViewerEntryPoint.nodeBuilder(this, buildContext)
.avatar(
diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt
index e3ce329b09e..da86349adbd 100644
--- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt
+++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt
@@ -8,7 +8,6 @@
package io.element.android.features.userprofile.impl.root
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
@@ -21,7 +20,6 @@ import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.userprofile.shared.UserProfileView
-import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@@ -69,12 +67,6 @@ class UserProfileNode @AssistedInject constructor(
val state = presenter.present()
- LaunchedEffect(state.startDmActionState) {
- val result = state.startDmActionState
- if (result is AsyncAction.Success) {
- onStartDM(result.data)
- }
- }
UserProfileView(
state = state,
modifier = modifier,
diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt
index a4dc5a75976..3bd6c7c3a33 100644
--- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt
+++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt
@@ -75,6 +75,9 @@ fun UserProfileHeaderSection(
text = userId.value,
style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.secondary,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
textAlign = TextAlign.Center,
)
}
diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
index 6ad4e5c6c66..e2870d8f5e3 100644
--- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
+++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
@@ -69,7 +69,7 @@ fun UserProfileView(
isDebugBuild = state.isDebugBuild,
avatarUrl = state.avatarUrl,
userId = state.userId,
- userName = state.userName,
+ userName = state.userName ?: state.userId.extractedDisplayName,
isUserVerified = state.isVerified,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId.extractedDisplayName, avatarUrl)
diff --git a/features/userprofile/shared/src/main/res/values-it/translations.xml b/features/userprofile/shared/src/main/res/values-it/translations.xml
index daacd442554..277ed7e27ec 100644
--- a/features/userprofile/shared/src/main/res/values-it/translations.xml
+++ b/features/userprofile/shared/src/main/res/values-it/translations.xml
@@ -13,5 +13,7 @@
"Sblocca"
"Potrai vedere di nuovo tutti i suoi messaggi."
"Sblocca utente"
+ "Usa l\'app web per verificare questo utente."
+ "Verifica %1$s"
"Si è verificato un errore durante il tentativo di avviare una chat"
diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt
index ebd897d84c6..601e7cce165 100644
--- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt
+++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt
@@ -20,7 +20,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
@@ -37,7 +38,7 @@ class IncomingVerificationPresenter @AssistedInject constructor(
@Assisted private val navigator: IncomingVerificationNavigator,
private val sessionVerificationService: SessionVerificationService,
private val stateMachine: IncomingVerificationStateMachine,
- private val dateFormatter: LastMessageTimestampFormatter,
+ private val dateFormatter: DateFormatter,
) : Presenter {
@AssistedFactory
interface Factory {
@@ -59,7 +60,10 @@ class IncomingVerificationPresenter @AssistedInject constructor(
}
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
val formattedSignInTime = remember {
- dateFormatter.format(sessionVerificationRequestDetails.firstSeenTimestamp)
+ dateFormatter.format(
+ timestamp = sessionVerificationRequestDetails.firstSeenTimestamp,
+ mode = DateFormatterMode.TimeOrDate,
+ )
}
val step by remember {
derivedStateOf {
diff --git a/features/verifysession/impl/src/main/res/values-fi/translations.xml b/features/verifysession/impl/src/main/res/values-fi/translations.xml
index ec67f48d3a3..c011022eb70 100644
--- a/features/verifysession/impl/src/main/res/values-fi/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-fi/translations.xml
@@ -16,7 +16,7 @@
"Varmista, että alla olevat numerot vastaavat toisessa istunnossa näkyviä numeroita."
"Vertaa numeroita"
"Uusi kirjautumisesi on nyt vahvistettu. Sillä on pääsy salattuihin viesteihisi, ja muut käyttäjät näkevät sen luotettuna."
- "Käytä palautusavainta"
+ "Syötä palautusavain"
"Joko pyyntö aikakatkaistiin, pyyntö hylättiin tai vahvistus ei täsmännyt."
"Vahvista, että se olet sinä, jotta näet aiemmat salatut viestisi."
"Avaa laite, jossa olet jo kirjautuneena"
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 a057a73a65d..bd3f194e4b8 100644
--- a/features/verifysession/impl/src/main/res/values-fr/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml
@@ -22,7 +22,7 @@
"Ouvrir une session existante"
"Réessayer la vérification"
"Je suis prêt.e"
- "En attente de correspondance"
+ "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."
"Connecté"
diff --git a/features/verifysession/impl/src/main/res/values-hu/translations.xml b/features/verifysession/impl/src/main/res/values-hu/translations.xml
index 2f25c0006e0..7e786dfc6c6 100644
--- a/features/verifysession/impl/src/main/res/values-hu/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-hu/translations.xml
@@ -35,6 +35,10 @@
"Ellenőrzés kérve"
"Nem egyeznek"
"Megegyeznek"
+ "Győződjön meg róla, hogy az alkalmazás nyitva van a másik eszközön, mielőtt innen elindítja az ellenőrzést."
+ "Nyissa meg az alkalmazást egy másik ellenőrzött eszközön"
+ "A másik eszközön egy felugró ablaknak kell megjelennie. Kezdje el az ellenőrzést onnan."
+ "Ellenőrzés megkezdése a másik eszközön"
"A folytatáshoz fogadja el az ellenőrzési folyamat indítási kérését a másik munkamenetében."
"Várakozás a kérés elfogadására"
"Kijelentkezés…"
diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml
index c1154a8c89f..322ab0e6d32 100644
--- a/features/verifysession/impl/src/main/res/values-it/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-it/translations.xml
@@ -17,6 +17,7 @@
"Confronta i numeri"
"La tua nuova sessione è ora verificata. Ha accesso ai tuoi messaggi crittografati e gli altri utenti la vedranno come attendibile."
"Inserisci la chiave di recupero"
+ "La richiesta è scaduta, è stata rifiutata o c\'è stata una mancata corrispondenza nella verifica."
"Dimostra la tua identità per accedere alla cronologia dei messaggi crittografati."
"Apri una sessione esistente"
"Riprova la verifica"
@@ -24,8 +25,20 @@
"In attesa di un riscontro"
"Confronta un set unico di emoji."
"Confronta le emoji uniche, assicurandoti che appaiano nello stesso ordine."
+ "Accesso effettuato"
+ "La richiesta è scaduta, è stata rifiutata o c\'è stata una mancata corrispondenza nella verifica."
+ "Verifica fallita"
+ "Continua solo se tu hai avviato questa verifica."
+ "Verifica l\'altro dispositivo per proteggere la cronologia dei messaggi."
+ "Ora puoi leggere o inviare messaggi in modo sicuro sull\'altro dispositivo."
+ "Dispositivo verificato"
+ "Richiesta di verifica"
"Non corrispondono"
"Corrispondono"
+ "Assicurati di avere l\'app aperta sull\'altro dispositivo prima di iniziare la verifica da qui."
+ "Apri l\'app su un altro dispositivo verificato"
+ "Dovresti vedere un popup sull\'altro dispositivo. Inizia subito la verifica da lì."
+ "Avvia la verifica sull\'altro dispositivo"
"Accetta la richiesta di avviare il processo di verifica nell\'altra sessione per continuare."
"In attesa di accettare la richiesta"
"Disconnessione in corso…"
diff --git a/features/verifysession/impl/src/main/res/values-pl/translations.xml b/features/verifysession/impl/src/main/res/values-pl/translations.xml
index 46c24d253e5..caa5471b963 100644
--- a/features/verifysession/impl/src/main/res/values-pl/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-pl/translations.xml
@@ -12,7 +12,7 @@
"Oczekiwanie na inne urządzenie…"
"Coś tu nie gra. Albo upłynął limit czasu, albo żądanie zostało odrzucone."
"Upewnij się, że emoji poniżej pasują do tych pokazanych na innej sesji."
- "Porównaj emotki"
+ "Porównaj emoji"
"Upewnij się, że liczby poniżej pasują do tych wyświetlanych na innej sesji."
"Porównaj liczby"
"Twoja nowa sesja jest teraz zweryfikowana. Ma ona dostęp do Twoich zaszyfrowanych wiadomości, a inni użytkownicy będą widzieć ją jako zaufaną."
@@ -24,7 +24,7 @@
"Jestem gotowy(a)"
"Oczekiwanie na dopasowanie"
"Porównaj unikalny zestaw emoji."
- "Porównaj unikalne emoji, upewniając się, że pojawiły się w tej samej kolejności."
+ "Porównaj unikalny zestaw emoji i upewnij się, że są w tej samej kolejności."
"Zalogowano"
"Albo upłynął limit czasu żądania, albo żądanie zostało odrzucone, albo wystąpił błąd weryfikacji."
"Weryfikacja nie powiodła się"
@@ -36,6 +36,6 @@
"Nie pasują do siebie"
"Pasują do siebie"
"Zaakceptuj prośbę o rozpoczęcie procesu weryfikacji w innej sesji, aby kontynuować."
- "Oczekiwanie na zaakceptowanie żądania"
+ "Oczekiwanie na zaakceptowanie prośby"
"Wylogowywanie…"
diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt
index 773b7b390b9..c4406009da0 100644
--- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt
+++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt
@@ -9,9 +9,8 @@ package io.element.android.features.verifysession.impl.incoming
import com.google.common.truth.Truth.assertThat
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
-import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
-import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -56,7 +55,7 @@ class IncomingVerificationPresenterTest {
IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID,
- formattedSignInTime = A_FORMATTED_DATE,
+ formattedSignInTime = "567 TimeOrDate false",
isWaiting = false,
)
)
@@ -119,7 +118,7 @@ class IncomingVerificationPresenterTest {
IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID,
- formattedSignInTime = A_FORMATTED_DATE,
+ formattedSignInTime = "567 TimeOrDate false",
isWaiting = false,
)
)
@@ -178,7 +177,7 @@ class IncomingVerificationPresenterTest {
IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID,
- formattedSignInTime = A_FORMATTED_DATE,
+ formattedSignInTime = "567 TimeOrDate false",
isWaiting = false,
)
)
@@ -210,7 +209,7 @@ class IncomingVerificationPresenterTest {
IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID,
- formattedSignInTime = A_FORMATTED_DATE,
+ formattedSignInTime = "567 TimeOrDate false",
isWaiting = false,
)
)
@@ -281,7 +280,7 @@ class IncomingVerificationPresenterTest {
sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails,
navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
service: SessionVerificationService = FakeSessionVerificationService(),
- dateFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE),
+ dateFormatter: DateFormatter = FakeDateFormatter(),
) = IncomingVerificationPresenter(
sessionVerificationRequestDetails = sessionVerificationRequestDetails,
navigator = navigator,
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 513bdc9328b..4797bdd92f2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,10 +3,10 @@
[versions]
# Project
-android_gradle_plugin = "8.7.2"
-kotlin = "2.0.21"
+android_gradle_plugin = "8.7.3"
+kotlin = "2.1.0"
kotlinpoet = "2.0.0"
-ksp = "2.0.21-1.0.28"
+ksp = "2.1.0-1.0.29"
firebaseAppDistribution = "5.0.0"
# AndroidX
@@ -21,18 +21,18 @@ constraintlayout = "2.2.0"
constraintlayout_compose = "1.1.0"
lifecycle = "2.8.7"
activity = "1.9.3"
-media3 = "1.5.0"
-camera = "1.4.0"
+media3 = "1.5.1"
+camera = "1.4.1"
# Compose
-compose_bom = "2024.11.00"
+compose_bom = "2024.12.01"
composecompiler = "1.5.15"
# Coroutines
coroutines = "1.9.0"
# Accompanist
-accompanist = "0.36.0"
+accompanist = "0.37.0"
# Test
test_core = "1.6.1"
@@ -50,10 +50,10 @@ wysiwyg = "2.37.14"
telephoto = "0.14.0"
# Dependency analysis
-dependencyAnalysis = "2.5.0"
+dependencyAnalysis = "2.6.1"
# DI
-dagger = "2.53"
+dagger = "2.53.1"
anvil = "0.4.0"
# Auto service
@@ -61,7 +61,7 @@ autoservice = "1.1.1"
# quality
androidx-test-ext-junit = "1.2.1"
-kover = "0.8.3"
+kover = "0.9.0"
[libraries]
# Project
@@ -77,7 +77,7 @@ kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", ve
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
gms_google_services = "com.google.gms:google-services:4.4.2"
# https://firebase.google.com/docs/android/setup#available-libraries
-google_firebase_bom = "com.google.firebase:firebase-bom:33.6.0"
+google_firebase_bom = "com.google.firebase:firebase-bom:33.7.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
@@ -150,7 +150,7 @@ test_arch_core = "androidx.arch.core:core-testing:2.2.0"
test_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.6.2"
test_mockk = "io.mockk:mockk:1.13.13"
-test_konsist = "com.lemonappdev:konsist:0.17.1"
+test_konsist = "com.lemonappdev:konsist:0.17.3"
test_turbine = "app.cash.turbine:turbine:1.2.0"
test_truth = "com.google.truth:truth:1.4.4"
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.18"
@@ -169,11 +169,11 @@ serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8"
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
-jsoup = "org.jsoup:jsoup:1.18.1"
+jsoup = "org.jsoup:jsoup:1.18.3"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1"
-matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.68"
+matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.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" }
@@ -187,15 +187,15 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2"
-maplibre = "org.maplibre.gl:android-sdk:11.6.1"
+maplibre = "org.maplibre.gl:android-sdk:11.7.1"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.1.0"
zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
# Analytics
-posthog = "com.posthog:posthog-android:3.9.2"
-sentry = "io.sentry:sentry-android:7.18.0"
+posthog = "com.posthog:posthog-android:3.9.3"
+sentry = "io.sentry:sentry-android:7.19.0"
# main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
@@ -235,7 +235,7 @@ anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
detekt = "io.gitlab.arturbosch.detekt:1.23.7"
ktlint = "org.jlleitschuh.gradle.ktlint:12.1.2"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
-dependencycheck = "org.owasp.dependencycheck:11.1.0"
+dependencycheck = "org.owasp.dependencycheck:11.1.1"
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }
paparazzi = "app.cash.paparazzi:1.3.5"
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index fb602ee2af0..eb1a55be0e1 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=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionSha256Sum=f397b287023acdba1e9f6fc5ea72d22dd63669d59ed4a289a29b1a76eee151c6
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt
index 7c282b13d80..688db472887 100644
--- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt
@@ -10,10 +10,13 @@ package io.element.android.libraries.androidutils.browser
import android.app.Activity
import android.content.ActivityNotFoundException
import android.net.Uri
+import android.os.Bundle
+import android.provider.Browser
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsSession
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
+import java.util.Locale
/**
* Open url in custom tab or, if not available, in the default browser.
@@ -51,6 +54,9 @@ fun Activity.openUrlInChromeCustomTab(
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true)
// Disable bookmark button
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true)
+ intent.putExtra(Browser.EXTRA_HEADERS, Bundle().apply {
+ putString("Accept-Language", Locale.getDefault().toLanguageTag())
+ })
}
.launchUrl(this, Uri.parse(url))
} catch (activityNotFoundException: ActivityNotFoundException) {
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
index 8287e2d19d8..e16985a1d2b 100644
--- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
@@ -7,6 +7,9 @@
package io.element.android.libraries.core.extensions
+import java.text.Normalizer
+import java.util.Locale
+
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
fun Boolean.to01() = if (this) "1" else "0"
@@ -61,3 +64,28 @@ fun String.replacePrefix(oldPrefix: String, newPrefix: String): String {
this
}
}
+
+/**
+ * Surround with brackets.
+ */
+fun String.withBrackets(prefix: String = "(", suffix: String = ")"): String {
+ return "$prefix$this$suffix"
+}
+
+/**
+ * Capitalize the string.
+ */
+fun String.safeCapitalize(): String {
+ return replaceFirstChar {
+ if (it.isLowerCase()) {
+ it.titlecase(Locale.getDefault())
+ } else {
+ it.toString()
+ }
+ }
+}
+
+fun String.withoutAccents(): String {
+ return Normalizer.normalize(this, Normalizer.Form.NFD)
+ .replace("\\p{Mn}+".toRegex(), "")
+}
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/List.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/List.kt
new file mode 100644
index 00000000000..0dee04408a4
--- /dev/null
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/List.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.core.extensions
+
+/**
+ * Returns the first element if the list contains exactly one element, otherwise returns null.
+ */
+inline fun List.firstIfSingle(): T? {
+ return if (size == 1) first() else null
+}
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/preview/PreviewUtil.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/preview/PreviewUtil.kt
new file mode 100644
index 00000000000..e4b20fd42b4
--- /dev/null
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/preview/PreviewUtil.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.core.preview
+
+val loremIpsum = """
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut la
+ bore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate v
+ elit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proide
+ nt, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ """.trimIndent()
diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt
new file mode 100644
index 00000000000..5632962582a
--- /dev/null
+++ b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.api
+
+interface DateFormatter {
+ fun format(
+ timestamp: Long?,
+ mode: DateFormatterMode = DateFormatterMode.Full,
+ useRelative: Boolean = false,
+ ): String
+}
+
+enum class DateFormatterMode {
+ /**
+ * Full date and time.
+ * Example:
+ * "April 6, 1980 at 6:35 PM"
+ * Format can be shorter when useRelative is true.
+ * Example:
+ * "6:35 PM"
+ */
+ Full,
+
+ /**
+ * Only month and year.
+ * Example:
+ * "April 1980"
+ * "This month" can be returned when useRelative is true.
+ * Example:
+ * "This month"
+ */
+ Month,
+
+ /**
+ * Only day.
+ * Example:
+ * "Sunday 6 April"
+ * "Today", "Yesterday" and day of week can be returned when useRelative is true.
+ */
+ Day,
+
+ /**
+ * Time if same day, else date.
+ */
+ TimeOrDate,
+
+ /**
+ * Only time whatever the day.
+ */
+ TimeOnly,
+}
diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DaySeparatorFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DaySeparatorFormatter.kt
deleted file mode 100644
index 4cc35218a0f..00000000000
--- a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DaySeparatorFormatter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.libraries.dateformatter.api
-
-interface DaySeparatorFormatter {
- fun format(timestamp: Long): String
-}
diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt
deleted file mode 100644
index c5b9778669b..00000000000
--- a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.libraries.dateformatter.api
-
-fun interface LastMessageTimestampFormatter {
- fun format(timestamp: Long?): String
-}
diff --git a/libraries/dateformatter/impl/build.gradle.kts b/libraries/dateformatter/impl/build.gradle.kts
index eb05eb18e0e..2fb4f8461fd 100644
--- a/libraries/dateformatter/impl/build.gradle.kts
+++ b/libraries/dateformatter/impl/build.gradle.kts
@@ -8,7 +8,7 @@ import extension.setupAnvil
*/
plugins {
- id("io.element.android-library")
+ id("io.element.android-compose-library")
}
setupAnvil()
@@ -16,15 +16,30 @@ setupAnvil()
android {
namespace = "io.element.android.libraries.dateformatter.impl"
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+
dependencies {
implementation(libs.dagger)
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.designsystem)
implementation(projects.libraries.di)
+ implementation(projects.libraries.uiStrings)
+ implementation(projects.services.toolbox.api)
api(projects.libraries.dateformatter.api)
api(libs.datetime)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
+ testImplementation(libs.test.turbine)
+ testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.dateformatter.test)
+ testImplementation(projects.services.toolbox.test)
+ testImplementation(projects.tests.testutils)
+ testImplementation(libs.androidx.compose.ui.test.junit)
}
}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt
new file mode 100644
index 00000000000..2f34d480e0c
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.core.extensions.safeCapitalize
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+interface DateFormatterDay {
+ fun format(
+ timestamp: Long,
+ useRelative: Boolean,
+ ): String
+}
+
+@ContributesBinding(AppScope::class)
+class DefaultDateFormatterDay @Inject constructor(
+ private val localDateTimeProvider: LocalDateTimeProvider,
+ private val dateFormatters: DateFormatters,
+) : DateFormatterDay {
+ override fun format(
+ timestamp: Long,
+ useRelative: Boolean,
+ ): String {
+ val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
+ val today = localDateTimeProvider.providesNow()
+ return if (useRelative) {
+ val dayDiff = today.date.toEpochDays() - dateToFormat.date.toEpochDays()
+ when (dayDiff) {
+ 0 -> dateFormatters.getRelativeDay(timestamp, "Today")
+ 1 -> dateFormatters.getRelativeDay(timestamp, "Yesterday")
+ else -> if (dayDiff < 7) {
+ dateFormatters.formatDateWithDay(dateToFormat)
+ } else {
+ if (today.year == dateToFormat.year) {
+ dateFormatters.formatDateWithFullFormatNoYear(dateToFormat)
+ } else {
+ dateFormatters.formatDateWithFullFormat(dateToFormat)
+ }
+ }
+ }
+ } else {
+ if (today.year == dateToFormat.year) {
+ dateFormatters.formatDateWithFullFormatNoYear(dateToFormat)
+ } else {
+ dateFormatters.formatDateWithFullFormat(dateToFormat)
+ }
+ }
+ .safeCapitalize()
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt
new file mode 100644
index 00000000000..80e613e38ee
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import io.element.android.services.toolbox.api.strings.StringProvider
+import javax.inject.Inject
+
+class DateFormatterFull @Inject constructor(
+ private val stringProvider: StringProvider,
+ private val localDateTimeProvider: LocalDateTimeProvider,
+ private val dateFormatters: DateFormatters,
+ private val dateFormatterDay: DateFormatterDay,
+) {
+ fun format(
+ timestamp: Long,
+ useRelative: Boolean,
+ ): String {
+ val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
+ val time = dateFormatters.formatTime(dateToFormat)
+ return if (useRelative) {
+ val now = localDateTimeProvider.providesNow()
+ if (now.date == dateToFormat.date) {
+ time
+ } else {
+ val dateStr = dateFormatterDay.format(timestamp, true)
+ stringProvider.getString(R.string.common_date_date_at_time, dateStr, time)
+ }
+ } else {
+ val dateStr = dateFormatters.formatDateWithFullFormat(dateToFormat)
+ stringProvider.getString(R.string.common_date_date_at_time, dateStr, time)
+ }
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt
new file mode 100644
index 00000000000..3d56ebcea1c
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import io.element.android.libraries.core.extensions.safeCapitalize
+import io.element.android.services.toolbox.api.strings.StringProvider
+import javax.inject.Inject
+
+class DateFormatterMonth @Inject constructor(
+ private val stringProvider: StringProvider,
+ private val localDateTimeProvider: LocalDateTimeProvider,
+ private val dateFormatters: DateFormatters,
+) {
+ fun format(
+ timestamp: Long,
+ useRelative: Boolean,
+ ): String {
+ val today = localDateTimeProvider.providesNow()
+ val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
+ return if (useRelative && dateToFormat.month == today.month && dateToFormat.year == today.year) {
+ stringProvider.getString(R.string.common_date_this_month)
+ } else {
+ dateFormatters.formatDateWithMonthAndYear(dateToFormat)
+ }
+ .safeCapitalize()
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatter.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt
similarity index 62%
rename from libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatter.kt
rename to libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt
index 8c34905836b..b0ad28fdcfa 100644
--- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatter.kt
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023, 2024 New Vector Ltd.
+ * Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
@@ -7,18 +7,16 @@
package io.element.android.libraries.dateformatter.impl
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
-import io.element.android.libraries.di.AppScope
import javax.inject.Inject
-@ContributesBinding(AppScope::class)
-class DefaultLastMessageTimestampFormatter @Inject constructor(
+class DateFormatterTime @Inject constructor(
private val localDateTimeProvider: LocalDateTimeProvider,
private val dateFormatters: DateFormatters,
-) : LastMessageTimestampFormatter {
- override fun format(timestamp: Long?): String {
- if (timestamp == null) return ""
+) {
+ fun format(
+ timestamp: Long,
+ useRelative: Boolean,
+ ): String {
val currentDate = localDateTimeProvider.providesNow()
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
val isSameDay = currentDate.date == dateToFormat.date
@@ -30,7 +28,7 @@ class DefaultLastMessageTimestampFormatter @Inject constructor(
dateFormatters.formatDate(
dateToFormat = dateToFormat,
currentDate = currentDate,
- useRelative = true
+ useRelative = useRelative,
)
}
}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt
new file mode 100644
index 00000000000..ce412f0d435
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import javax.inject.Inject
+
+class DateFormatterTimeOnly @Inject constructor(
+ private val localDateTimeProvider: LocalDateTimeProvider,
+ private val dateFormatters: DateFormatters,
+) {
+ fun format(
+ timestamp: Long,
+ ): String {
+ val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
+ return dateFormatters.formatTime(dateToFormat)
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
index a78cc81c24c..a041952fc37 100644
--- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
@@ -7,57 +7,64 @@
package io.element.android.libraries.dateformatter.impl
-import android.text.format.DateFormat
import android.text.format.DateUtils
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.SingleIn
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaLocalDateTime
+import timber.log.Timber
import java.time.Period
-import java.time.format.DateTimeFormatter
-import java.time.format.FormatStyle
import java.util.Locale
import javax.inject.Inject
import kotlin.math.absoluteValue
+@SingleIn(AppScope::class)
class DateFormatters @Inject constructor(
- private val locale: Locale,
+ localeChangeObserver: LocaleChangeObserver,
private val clock: Clock,
private val timeZoneProvider: TimezoneProvider,
-) {
- private val onlyTimeFormatter: DateTimeFormatter by lazy {
- DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
+ locale: Locale,
+) : LocaleChangeListener {
+ init {
+ localeChangeObserver.addListener(this)
}
- private val dateWithMonthFormatter: DateTimeFormatter by lazy {
- val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
- DateTimeFormatter.ofPattern(pattern, locale)
- }
+ private var dateTimeFormatters: DateTimeFormatters = DateTimeFormatters(locale)
- private val dateWithYearFormatter: DateTimeFormatter by lazy {
- val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
- DateTimeFormatter.ofPattern(pattern, locale)
+ override fun onLocaleChange() {
+ Timber.w("Locale changed, updating formatters")
+ dateTimeFormatters = DateTimeFormatters(Locale.getDefault())
}
- private val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
- DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale)
+ internal fun formatTime(localDateTime: LocalDateTime): String {
+ return dateTimeFormatters.onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
}
- internal fun formatTime(localDateTime: LocalDateTime): String {
- return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
+ internal fun formatDateWithMonthAndYear(localDateTime: LocalDateTime): String {
+ return dateTimeFormatters.dateWithMonthAndYearFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithMonth(localDateTime: LocalDateTime): String {
- return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
+ return dateTimeFormatters.dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
+ }
+
+ internal fun formatDateWithDay(localDateTime: LocalDateTime): String {
+ return dateTimeFormatters.dateWithDayFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithYear(localDateTime: LocalDateTime): String {
- return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
+ return dateTimeFormatters.dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
- return dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
+ return dateTimeFormatters.dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
+ }
+
+ internal fun formatDateWithFullFormatNoYear(localDateTime: LocalDateTime): String {
+ return dateTimeFormatters.dateWithFullFormatNoYearFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDate(
@@ -75,12 +82,12 @@ class DateFormatters @Inject constructor(
}
}
- private fun getRelativeDay(ts: Long): String {
+ internal fun getRelativeDay(ts: Long, default: String = ""): String {
return DateUtils.getRelativeTimeSpanString(
ts,
clock.now().toEpochMilliseconds(),
DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY
- )?.toString() ?: ""
+ )?.toString() ?: default
}
}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateTimeFormatters.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateTimeFormatters.kt
new file mode 100644
index 00000000000..15dc6aa05eb
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateTimeFormatters.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import android.text.format.DateFormat
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import java.util.Locale
+
+class DateTimeFormatters(
+ private val locale: Locale,
+) {
+ val onlyTimeFormatter: DateTimeFormatter by lazy {
+ DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
+ }
+
+ val dateWithMonthAndYearFormatter: DateTimeFormatter by lazy {
+ val pattern = bestDateTimePattern("MMMM YYYY")
+ DateTimeFormatter.ofPattern(pattern, locale)
+ }
+
+ val dateWithMonthFormatter: DateTimeFormatter by lazy {
+ val pattern = bestDateTimePattern("d MMM")
+ DateTimeFormatter.ofPattern(pattern, locale)
+ }
+
+ val dateWithDayFormatter: DateTimeFormatter by lazy {
+ val pattern = bestDateTimePattern("EEEE")
+ DateTimeFormatter.ofPattern(pattern, locale)
+ }
+
+ val dateWithYearFormatter: DateTimeFormatter by lazy {
+ val pattern = bestDateTimePattern("dd.MM.yyyy")
+ DateTimeFormatter.ofPattern(pattern, locale)
+ }
+
+ val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(locale)
+ }
+
+ val dateWithFullFormatNoYearFormatter: DateTimeFormatter by lazy {
+ val pattern = DateFormat.getBestDateTimePattern(locale, "EEEE d MMMM") ?: "EEEE d MMMM"
+ DateTimeFormatter.ofPattern(pattern, locale)
+ }
+
+ private fun bestDateTimePattern(pattern: String): String {
+ return DateFormat.getBestDateTimePattern(locale, pattern) ?: pattern
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt
new file mode 100644
index 00000000000..7497f8ee453
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultDateFormatter @Inject constructor(
+ private val dateFormatterFull: DateFormatterFull,
+ private val dateFormatterMonth: DateFormatterMonth,
+ private val dateFormatterDay: DateFormatterDay,
+ private val dateFormatterTime: DateFormatterTime,
+ private val dateFormatterTimeOnly: DateFormatterTimeOnly,
+) : DateFormatter {
+ override fun format(
+ timestamp: Long?,
+ mode: DateFormatterMode,
+ useRelative: Boolean,
+ ): String {
+ timestamp ?: return ""
+ return when (mode) {
+ DateFormatterMode.Full -> {
+ dateFormatterFull.format(timestamp, useRelative)
+ }
+ DateFormatterMode.Month -> {
+ dateFormatterMonth.format(timestamp, useRelative)
+ }
+ DateFormatterMode.Day -> {
+ dateFormatterDay.format(timestamp, useRelative)
+ }
+ DateFormatterMode.TimeOrDate -> {
+ dateFormatterTime.format(timestamp, useRelative)
+ }
+ DateFormatterMode.TimeOnly -> {
+ dateFormatterTimeOnly.format(timestamp)
+ }
+ }
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt
deleted file mode 100644
index 89ef9ee412a..00000000000
--- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.libraries.dateformatter.impl
-
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
-import io.element.android.libraries.di.AppScope
-import javax.inject.Inject
-
-@ContributesBinding(AppScope::class)
-class DefaultDaySeparatorFormatter @Inject constructor(
- private val localDateTimeProvider: LocalDateTimeProvider,
- private val dateFormatters: DateFormatters,
-) : DaySeparatorFormatter {
- override fun format(timestamp: Long): String {
- val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
- // TODO use relative formatting once iOS uses it too
- return dateFormatters.formatDateWithFullFormat(dateToFormat)
- }
-}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt
new file mode 100644
index 00000000000..e89bfe7a991
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.di.SingleIn
+import javax.inject.Inject
+
+fun interface LocaleChangeObserver {
+ fun addListener(listener: LocaleChangeListener)
+}
+
+interface LocaleChangeListener {
+ fun onLocaleChange()
+}
+
+@SingleIn(AppScope::class)
+@ContributesBinding(AppScope::class)
+class DefaultLocaleChangeObserver @Inject constructor(
+ @ApplicationContext private val context: Context,
+) : LocaleChangeObserver {
+ init {
+ registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ listeners.forEach(LocaleChangeListener::onLocaleChange)
+ }
+ })
+ }
+
+ private val listeners = mutableSetOf()
+
+ override fun addListener(listener: LocaleChangeListener) {
+ listeners.add(listener)
+ }
+
+ private fun registerReceiver(receiver: BroadcastReceiver) {
+ val filter = IntentFilter()
+ filter.addAction(Intent.ACTION_LOCALE_CHANGED)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ filter.addAction(Intent.ACTION_APPLICATION_LOCALE_CHANGED)
+ }
+ context.registerReceiver(receiver, filter)
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt
new file mode 100644
index 00000000000..5b9f732ceb3
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+data class DateForPreview(
+ val semantic: String,
+ val date: String,
+)
+
+val dateForPreviewToday = DateForPreview(
+ semantic = "Today",
+ date = "1980-04-06T18:35:24.00Z",
+)
+
+val dateForPreviews = listOf(
+ DateForPreview(
+ semantic = "Now",
+ date = dateForPreviewToday.date,
+ ),
+ DateForPreview(
+ semantic = "One second ago",
+ date = "1980-04-06T18:35:23.00Z",
+ ),
+ DateForPreview(
+ semantic = "One minute ago",
+ date = "1980-04-06T18:34:24.00Z",
+ ),
+ DateForPreview(
+ semantic = "One hour ago",
+ date = "1980-04-06T17:35:24.00Z",
+ ),
+ DateForPreview(
+ semantic = "One day ago",
+ date = "1980-04-05T18:35:24.00Z",
+ ),
+ DateForPreview(
+ semantic = "Two days ago",
+ date = "1980-04-04T18:35:24.00Z",
+ ),
+ DateForPreview(
+ semantic = "One month ago",
+ date = "1980-03-06T18:35:24.00Z",
+ ),
+ DateForPreview(
+ semantic = "One year ago",
+ date = "1979-04-06T18:35:24.00Z",
+ ),
+)
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt
new file mode 100644
index 00000000000..36d7acabfc2
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+
+class DateFormatterModeProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = DateFormatterMode.entries.asSequence()
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt
new file mode 100644
index 00000000000..d12f7b07247
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.tooling.preview.Preview
+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.dateformatter.api.DateFormatterMode
+import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.utils.allBooleans
+import kotlinx.datetime.Instant
+
+@Preview
+@Composable
+internal fun DateFormatterModeViewPreview(
+ @PreviewParameter(DateFormatterModeProvider::class) dateFormatterMode: DateFormatterMode,
+) = ElementPreview {
+ DateFormatterModeView(dateFormatterMode)
+}
+
+@Composable
+private fun DateFormatterModeView(
+ mode: DateFormatterMode,
+) {
+ val context = LocalContext.current
+ val composeLocale = Locale.current
+ val dateFormatter = remember {
+ createFormatter(
+ context = context,
+ currentDate = dateForPreviewToday.date,
+ locale = java.util.Locale.Builder()
+ .setLanguageTag(composeLocale.toLanguageTag())
+ .build(),
+ )
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(4.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = "Mode $mode / $composeLocale",
+ style = ElementTheme.typography.fontHeadingSmMedium
+ )
+ val today = Instant.parse(dateForPreviewToday.date).toEpochMilliseconds()
+ Text(
+ text = "Today is: ${dateFormatter.format(today, DateFormatterMode.Full, useRelative = false)}",
+ style = ElementTheme.typography.fontHeadingSmMedium,
+ )
+ dateForPreviews.forEach { dateForPreview ->
+ DateForPreviewItem(
+ dateForPreview = dateForPreview,
+ dateFormatter = dateFormatter,
+ mode = mode,
+ )
+ }
+ }
+}
+
+@Composable
+private fun DateForPreviewItem(
+ dateForPreview: DateForPreview,
+ dateFormatter: DefaultDateFormatter,
+ mode: DateFormatterMode,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(2.dp),
+ ) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 8.dp),
+ text = dateForPreview.semantic,
+ style = ElementTheme.typography.fontBodyMdMedium,
+ color = ElementTheme.colors.textSecondary,
+ )
+ val ts = Instant.parse(dateForPreview.date).toEpochMilliseconds()
+ Row {
+ Column {
+ listOf("Absolute:", "Relative:").forEach { label ->
+ Text(
+ text = label,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textPrimary,
+ )
+ }
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Column {
+ allBooleans.forEach { useRelative ->
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = dateFormatter.format(ts, mode, useRelative),
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textPrimary,
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt
new file mode 100644
index 00000000000..cf9787e9d3b
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import android.content.Context
+import io.element.android.libraries.dateformatter.impl.DateFormatterFull
+import io.element.android.libraries.dateformatter.impl.DateFormatterMonth
+import io.element.android.libraries.dateformatter.impl.DateFormatterTime
+import io.element.android.libraries.dateformatter.impl.DateFormatterTimeOnly
+import io.element.android.libraries.dateformatter.impl.DateFormatters
+import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
+import io.element.android.libraries.dateformatter.impl.DefaultDateFormatterDay
+import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import java.util.Locale
+
+/**
+ * Create DefaultDateFormatter and set current time to the provided date.
+ */
+fun createFormatter(
+ context: Context,
+ currentDate: String,
+ locale: Locale,
+): DefaultDateFormatter {
+ val clock = PreviewClock().apply { givenInstant(Instant.parse(currentDate)) }
+ val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
+ val dateFormatters = DateFormatters(
+ localeChangeObserver = {},
+ clock = clock,
+ timeZoneProvider = { TimeZone.UTC },
+ locale = locale,
+ )
+ val stringProvider = PreviewStringProvider(context.resources)
+ val dateFormatterDay = DefaultDateFormatterDay(
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ )
+ return DefaultDateFormatter(
+ dateFormatterFull = DateFormatterFull(
+ stringProvider = stringProvider,
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ dateFormatterDay = dateFormatterDay,
+ ),
+ dateFormatterMonth = DateFormatterMonth(
+ stringProvider = stringProvider,
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ ),
+ dateFormatterDay = dateFormatterDay,
+ dateFormatterTime = DateFormatterTime(
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ ),
+ dateFormatterTimeOnly = DateFormatterTimeOnly(
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ ),
+ )
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt
new file mode 100644
index 00000000000..3486d169a21
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+
+class PreviewClock : Clock {
+ private var instant: Instant = Instant.fromEpochMilliseconds(0)
+
+ fun givenInstant(instant: Instant) {
+ this.instant = instant
+ }
+
+ override fun now(): Instant = instant
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt
new file mode 100644
index 00000000000..6498b30d885
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import android.content.res.Resources
+import androidx.annotation.PluralsRes
+import androidx.annotation.StringRes
+import io.element.android.services.toolbox.api.strings.StringProvider
+
+class PreviewStringProvider(
+ private val resources: Resources
+) : StringProvider {
+ override fun getString(@StringRes resId: Int): String {
+ return resources.getString(resId)
+ }
+
+ override fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
+ return resources.getString(resId, *formatArgs)
+ }
+
+ override fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String {
+ return resources.getQuantityString(resId, quantity, *formatArgs)
+ }
+}
diff --git a/libraries/dateformatter/impl/src/main/res/values-cs/translations.xml b/libraries/dateformatter/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 00000000000..578211b754f
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "%1$s v %2$s"
+ "Tento měsíc"
+
diff --git a/libraries/dateformatter/impl/src/main/res/values-el/translations.xml b/libraries/dateformatter/impl/src/main/res/values-el/translations.xml
new file mode 100644
index 00000000000..3b337d1e290
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values-el/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Αυτό το μήνα"
+
diff --git a/libraries/dateformatter/impl/src/main/res/values-et/translations.xml b/libraries/dateformatter/impl/src/main/res/values-et/translations.xml
new file mode 100644
index 00000000000..896610a602e
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values-et/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Sel kuul"
+
diff --git a/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml b/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 00000000000..f2635367677
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "%1$s à %2$s"
+ "Ce mois-ci"
+
diff --git a/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml b/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml
new file mode 100644
index 00000000000..33778d84f18
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values-hu/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "%1$s itt: %2$s"
+ "Ebben a hónapban"
+
diff --git a/libraries/dateformatter/impl/src/main/res/values/localazy.xml b/libraries/dateformatter/impl/src/main/res/values/localazy.xml
new file mode 100644
index 00000000000..8b0dab8cff4
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,5 @@
+
+
+ "%1$s at %2$s"
+ "This month"
+
diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt
new file mode 100644
index 00000000000..8dd0c61d9fb
--- /dev/null
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import kotlinx.datetime.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(qualifiers = "fr")
+class DefaultDateFormatterFrTest {
+ @Test
+ fun `test null`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val ts: Long? = null
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts)).isEmpty()
+ }
+
+ @Test
+ fun `test epoch`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val ts = 0L
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("1 janvier 1970 à 00:00")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Janvier 1970")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("1 janvier 1970")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("00:00")
+ }
+
+ @Test
+ fun `test epoch relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val ts = 0L
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("1 janvier 1970 à 00:00")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Janvier 1970")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("1 janvier 1970")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("00:00")
+ }
+
+ @Test
+ fun `test now`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test now relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourd’hui")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test one second before`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:35:23.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test one second before relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:35:23.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourd’hui")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test one minute before`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:34:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:34")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:34")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:34")
+ }
+
+ @Test
+ fun `test one minute before relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:34:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:34")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourd’hui")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:34")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:34")
+ }
+
+ @Test
+ fun `test one hour before`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T17:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 17:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("17:35")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("17:35")
+ }
+
+ @Test
+ fun `test one hour before relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T17:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("17:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourd’hui")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("17:35")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("17:35")
+ }
+
+ @Test
+ fun `test one day before same time`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-05T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("5 avril 1980 à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Samedi 5 avril")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 avr.")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test one day before same time relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-05T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Hier à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Hier")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Hier")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test two days before same time`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-04T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("4 avril 1980 à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Vendredi 4 avril")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 avr.")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test two days before same time relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-04T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Vendredi à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Vendredi")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 avr.")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test one month before same time`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-03-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 mars 1980 à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Mars 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Jeudi 6 mars")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 mars")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test one month before same time relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-03-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Jeudi 6 mars à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Mars 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Jeudi 6 mars")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 mars")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test one year before same time`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1979-04-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1979 à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1979")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("6 avril 1979")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+ }
+
+ @Test
+ fun `test one year before same time relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1979-04-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6 avril 1979 à 18:35")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Avril 1979")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("6 avril 1979")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+ }
+}
diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt
new file mode 100644
index 00000000000..b7bf9d818eb
--- /dev/null
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import kotlinx.datetime.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(qualifiers = "en")
+class DefaultDateFormatterTest {
+ @Test
+ fun `test null`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val ts: Long? = null
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts)).isEmpty()
+ }
+
+ @Test
+ fun `test epoch`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val ts = 0L
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("January 1, 1970 at 12:00 AM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("January 1970")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("January 1, 1970")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("12:00 AM")
+ }
+
+ @Test
+ fun `test epoch relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val ts = 0L
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("January 1, 1970 at 12:00 AM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("January 1970")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("January 1, 1970")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("12:00 AM")
+ }
+
+ @Test
+ fun `test now`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test now relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test one second before`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:35:23.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test one second before relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:35:23.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test one minute before`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:34:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:34 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:34 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:34 PM")
+ }
+
+ @Test
+ fun `test one minute before relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T18:34:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:34 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:34 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:34 PM")
+ }
+
+ @Test
+ fun `test one hour before`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T17:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 5:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("5:35 PM")
+ }
+
+ @Test
+ fun `test one hour before relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-06T17:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("5:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("5:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("5:35 PM")
+ }
+
+ @Test
+ fun `test one day before same time`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-05T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 5, 1980 at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Saturday 5 April")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 Apr")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test one day before same time relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-05T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Yesterday at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Yesterday")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Yesterday")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test two days before same time`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-04T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 4, 1980 at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Friday 4 April")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 Apr")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test two days before same time relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-04-04T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Friday at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Friday")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 Apr")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test one month before same time`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-03-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("March 6, 1980 at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("March 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Thursday 6 March")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 Mar")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test one month before same time relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1980-03-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Thursday 6 March at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("March 1980")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Thursday 6 March")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 Mar")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test one year before same time`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1979-04-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1979 at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1979")
+ assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("April 6, 1979")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
+ }
+
+ @Test
+ fun `test one year before same time relative`() {
+ val now = "1980-04-06T18:35:24.00Z"
+ val dat = "1979-04-06T18:35:24.00Z"
+ val ts = Instant.parse(dat).toEpochMilliseconds()
+ val formatter = createFormatter(now)
+ assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("April 6, 1979 at 6:35 PM")
+ assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("April 1979")
+ assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("April 6, 1979")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
+ assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
+ }
+}
diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
deleted file mode 100644
index 5c8de4462b1..00000000000
--- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.libraries.dateformatter.impl
-
-import com.google.common.truth.Truth.assertThat
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
-import io.element.android.libraries.dateformatter.test.FakeClock
-import kotlinx.datetime.Instant
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.toLocalDateTime
-import org.junit.Test
-import java.util.Locale
-
-class DefaultLastMessageTimestampFormatterTest {
- @Test
- fun `test null`() {
- val now = "1980-04-06T18:35:24.00Z"
- val formatter = createFormatter(now)
- assertThat(formatter.format(null)).isEmpty()
- }
-
- @Test
- fun `test epoch`() {
- val now = "1980-04-06T18:35:24.00Z"
- val formatter = createFormatter(now)
- assertThat(formatter.format(0)).isEqualTo("01.01.1970")
- }
-
- @Test
- fun `test now`() {
- val now = "1980-04-06T18:35:24.00Z"
- val dat = "1980-04-06T18:35:24.00Z"
- val formatter = createFormatter(now)
- assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
- }
-
- @Test
- fun `test one second before`() {
- val now = "1980-04-06T18:35:24.00Z"
- val dat = "1980-04-06T18:35:23.00Z"
- val formatter = createFormatter(now)
- assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
- }
-
- @Test
- fun `test one minute before`() {
- val now = "1980-04-06T18:35:24.00Z"
- val dat = "1980-04-06T18:34:24.00Z"
- val formatter = createFormatter(now)
- assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:34 PM")
- }
-
- @Test
- fun `test one hour before`() {
- val now = "1980-04-06T18:35:24.00Z"
- val dat = "1980-04-06T17:35:24.00Z"
- val formatter = createFormatter(now)
- assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("5:35 PM")
- }
-
- @Test
- fun `test one day before same time`() {
- val now = "1980-04-06T18:35:24.00Z"
- val dat = "1980-04-05T18:35:24.00Z"
- val formatter = createFormatter(now)
- // TODO DateUtils.getRelativeTimeSpanString returns null.
- assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("")
- }
-
- @Test
- fun `test one month before same time`() {
- val now = "1980-04-06T18:35:24.00Z"
- val dat = "1980-03-06T18:35:24.00Z"
- val formatter = createFormatter(now)
- assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6 Mar")
- }
-
- @Test
- fun `test one year before same time`() {
- val now = "1980-04-06T18:35:24.00Z"
- val dat = "1979-04-06T18:35:24.00Z"
- val formatter = createFormatter(now)
- assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("06.04.1979")
- }
-
- @Test
- fun `test full format`() {
- val now = "1980-04-06T18:35:24.00Z"
- val dat = "1979-04-06T18:35:24.00Z"
- val clock = FakeClock().apply { givenInstant(Instant.parse(now)) }
- val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC }
- assertThat(dateFormatters.formatDateWithFullFormat(Instant.parse(dat).toLocalDateTime(TimeZone.UTC))).isEqualTo("Friday, April 6, 1979")
- }
-
- /**
- * Create DefaultLastMessageFormatter and set current time to the provided date.
- */
- private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageTimestampFormatter {
- val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
- val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
- val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC }
- return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters)
- }
-}
diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt
new file mode 100644
index 00000000000..dd1572fde67
--- /dev/null
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import io.element.android.tests.testutils.InstrumentationStringProvider
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import java.util.Locale
+
+/**
+ * Create DefaultDateFormatter and set current time to the provided date.
+ */
+fun createFormatter(currentDate: String): DefaultDateFormatter {
+ val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
+ val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
+ val dateFormatters = DateFormatters(
+ localeChangeObserver = {},
+ clock = clock,
+ timeZoneProvider = { TimeZone.UTC },
+ locale = Locale.getDefault(),
+ )
+ val stringProvider = InstrumentationStringProvider()
+ val dateFormatterDay = DefaultDateFormatterDay(
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ )
+ return DefaultDateFormatter(
+ dateFormatterFull = DateFormatterFull(
+ stringProvider = stringProvider,
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ dateFormatterDay = dateFormatterDay,
+ ),
+ dateFormatterMonth = DateFormatterMonth(
+ stringProvider = stringProvider,
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ ),
+ dateFormatterDay = dateFormatterDay,
+ dateFormatterTime = DateFormatterTime(
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ ),
+ dateFormatterTimeOnly = DateFormatterTimeOnly(
+ localDateTimeProvider = localDateTimeProvider,
+ dateFormatters = dateFormatters,
+ ),
+ )
+}
diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt
similarity index 88%
rename from libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt
rename to libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt
index 79e0eda10ff..c6bdbec73f4 100644
--- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.libraries.dateformatter.test
+package io.element.android.libraries.dateformatter.impl
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDateFormatter.kt b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDateFormatter.kt
new file mode 100644
index 00000000000..722e43f2c90
--- /dev/null
+++ b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDateFormatter.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.test
+
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+
+class FakeDateFormatter(
+ private val formatLambda: (Long?, DateFormatterMode, Boolean) -> String = { timestamp, mode, useRelative ->
+ "$timestamp $mode $useRelative"
+ },
+) : DateFormatter {
+ override fun format(
+ timestamp: Long?,
+ mode: DateFormatterMode,
+ useRelative: Boolean,
+ ): String {
+ return formatLambda(timestamp, mode, useRelative)
+ }
+}
diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDaySeparatorFormatter.kt b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDaySeparatorFormatter.kt
deleted file mode 100644
index 529d8848098..00000000000
--- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDaySeparatorFormatter.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.libraries.dateformatter.test
-
-import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
-
-class FakeDaySeparatorFormatter : DaySeparatorFormatter {
- private var format = ""
-
- fun givenFormat(format: String) {
- this.format = format
- }
-
- override fun format(timestamp: Long): String {
- return format
- }
-}
diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt
deleted file mode 100644
index 7edcf321cbe..00000000000
--- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright 2023, 2024 New Vector Ltd.
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- * Please see LICENSE in the repository root for full details.
- */
-
-package io.element.android.libraries.dateformatter.test
-
-import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
-
-const val A_FORMATTED_DATE = "formatted_date"
-
-class FakeLastMessageTimestampFormatter(
- var format: String = "",
-) : LastMessageTimestampFormatter {
- fun givenFormat(format: String) {
- this.format = format
- }
-
- override fun format(timestamp: Long?): String {
- return format
- }
-}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt
index 5b22b534f43..b0497620aa7 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt
@@ -10,9 +10,12 @@ package io.element.android.libraries.designsystem.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CatchingPokemon
@@ -48,8 +51,13 @@ object BigIcon {
*
* @param vectorIcon the [ImageVector] to display
* @param contentDescription the content description of the icon, if any. It defaults to `null`
+ * @param useCriticalTint whether the icon and background should be rendered using critical tint
*/
- data class Default(val vectorIcon: ImageVector, val contentDescription: String? = null) : Style
+ data class Default(
+ val vectorIcon: ImageVector,
+ val contentDescription: String? = null,
+ val useCriticalTint: Boolean = false,
+ ) : Style
/**
* An alert style with a transparent background.
@@ -84,25 +92,40 @@ object BigIcon {
modifier: Modifier = Modifier,
) {
val backgroundColor = when (style) {
- is Style.Default -> ElementTheme.colors.bgSubtleSecondary
- Style.Alert, Style.Success -> Color.Transparent
+ is Style.Default -> if (style.useCriticalTint) {
+ ElementTheme.colors.bgCriticalSubtle
+ } else {
+ ElementTheme.colors.bgSubtleSecondary
+ }
+ Style.Alert,
+ Style.Success -> Color.Transparent
Style.AlertSolid -> ElementTheme.colors.bgCriticalSubtle
Style.SuccessSolid -> ElementTheme.colors.bgSuccessSubtle
}
val icon = when (style) {
is Style.Default -> style.vectorIcon
- Style.Alert, Style.AlertSolid -> CompoundIcons.Error()
- Style.Success, Style.SuccessSolid -> CompoundIcons.CheckCircleSolid()
+ Style.Alert,
+ Style.AlertSolid -> CompoundIcons.Error()
+ Style.Success,
+ Style.SuccessSolid -> CompoundIcons.CheckCircleSolid()
}
val contentDescription = when (style) {
is Style.Default -> style.contentDescription
- Style.Alert, Style.AlertSolid -> stringResource(CommonStrings.common_error)
- Style.Success, Style.SuccessSolid -> stringResource(CommonStrings.common_success)
+ Style.Alert,
+ Style.AlertSolid -> stringResource(CommonStrings.common_error)
+ Style.Success,
+ Style.SuccessSolid -> stringResource(CommonStrings.common_success)
}
val iconTint = when (style) {
- is Style.Default -> ElementTheme.colors.iconSecondary
- Style.Alert, Style.AlertSolid -> ElementTheme.colors.iconCriticalPrimary
- Style.Success, Style.SuccessSolid -> ElementTheme.colors.iconSuccessPrimary
+ is Style.Default -> if (style.useCriticalTint) {
+ ElementTheme.colors.iconCriticalPrimary
+ } else {
+ ElementTheme.colors.iconSecondary
+ }
+ Style.Alert,
+ Style.AlertSolid -> ElementTheme.colors.iconCriticalPrimary
+ Style.Success,
+ Style.SuccessSolid -> ElementTheme.colors.iconSuccessPrimary
}
Box(
modifier = modifier
@@ -123,11 +146,19 @@ object BigIcon {
@PreviewsDayNight
@Composable
-internal fun BigIconPreview() {
- ElementPreview {
- Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(10.dp)) {
- val provider = BigIconStyleProvider()
- for (style in provider.values) {
+internal fun BigIconPreview() = ElementPreview {
+ LazyVerticalGrid(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(10.dp),
+ columns = GridCells.Adaptive(minSize = 64.dp),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ items(BigIconStyleProvider().values.toList()) { style ->
+ Box(
+ contentAlignment = Alignment.Center
+ ) {
BigIcon(style = style)
}
}
@@ -140,6 +171,7 @@ internal class BigIconStyleProvider : PreviewParameterProvider {
BigIcon.Style.Default(Icons.Filled.CatchingPokemon),
BigIcon.Style.Alert,
BigIcon.Style.AlertSolid,
+ BigIcon.Style.Default(Icons.Filled.CatchingPokemon, useCriticalTint = true),
BigIcon.Style.Success,
BigIcon.Style.SuccessSolid
)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
index 49a3e93e876..f3c9fb317a0 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt
@@ -54,4 +54,9 @@ enum class AvatarSize(val dp: Dp) {
EditProfileDetails(96.dp),
Suggestion(32.dp),
+
+ KnockRequestItem(52.dp),
+ KnockRequestBanner(32.dp),
+
+ MediaSender(32.dp),
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt
index e7fc1c5ce4d..40783998308 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/media/WaveformPlaybackView.kt
@@ -189,7 +189,7 @@ internal fun WaveformPlaybackViewPreview() = ElementPreview {
showCursor = false,
playbackProgress = 0.5f,
onSeek = {},
- waveform = persistentListOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f),
+ waveform = aWaveForm().toPersistentList(),
)
WaveformPlaybackView(
modifier = Modifier.height(34.dp),
@@ -219,3 +219,45 @@ private fun ImmutableList.normalisedData(maxSamplesCount: Int): Immutable
return result.toPersistentList()
}
+
+fun aWaveForm(): List {
+ return listOf(
+ 0.000f,
+ 0.000f,
+ 0.000f,
+ 0.003f,
+ 0.354f,
+ 0.353f,
+ 0.365f,
+ 0.790f,
+ 0.787f,
+ 0.167f,
+ 0.333f,
+ 0.975f,
+ 0.000f,
+ 0.102f,
+ 0.003f,
+ 0.531f,
+ 0.584f,
+ 0.317f,
+ 0.140f,
+ 0.475f,
+ 0.496f,
+ 0.561f,
+ 0.042f,
+ 0.263f,
+ 0.169f,
+ 0.829f,
+ 0.349f,
+ 0.010f,
+ 0.000f,
+ 0.000f,
+ 1.000f,
+ 0.334f,
+ 0.321f,
+ 0.011f,
+ 0.000f,
+ 0.000f,
+ 0.003f,
+ )
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsList.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsList.kt
index 28d7f845987..3822401b372 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsList.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/icons/IconsList.kt
@@ -13,9 +13,7 @@ import io.element.android.libraries.designsystem.R
// All the icons should be defined in Compound.
internal val iconsOther = listOf(
R.drawable.ic_cancel,
- R.drawable.ic_developer_options,
R.drawable.ic_encryption_enabled,
- R.drawable.ic_groups,
R.drawable.ic_notification_small,
R.drawable.ic_plus_composer,
R.drawable.ic_stop,
diff --git a/libraries/designsystem/src/main/res/drawable/ic_developer_options.xml b/libraries/designsystem/src/main/res/drawable/ic_developer_options.xml
deleted file mode 100644
index a55953010c8..00000000000
--- a/libraries/designsystem/src/main/res/drawable/ic_developer_options.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/libraries/designsystem/src/main/res/drawable/ic_groups.xml b/libraries/designsystem/src/main/res/drawable/ic_groups.xml
deleted file mode 100644
index 6f4b91e8dce..00000000000
--- a/libraries/designsystem/src/main/res/drawable/ic_groups.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
diff --git a/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml b/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml
index 879e21c466d..bfa63c86c32 100644
--- a/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml
+++ b/libraries/eventformatter/impl/src/main/res/values-hu/translations.xml
@@ -28,8 +28,8 @@
"%1$s meghívta"
"%1$s csatlakozott a szobához"
"Csatlakozott a szobához"
- "%1$s kérte, hogy csatlakozhasson"
- "%1$s engedélyezte, hogy %2$s csatlakozhasson"
+ "%1$s kéri, hogy csatlakozhasson"
+ "%1$s hozzáférést kapott a következőhöz: %2$s"
"Engedélyezte, hogy %1$s csatlakozhasson"
"Kérte, hogy csatlakozhasson"
"%1$s elutasította %2$s kérését, hogy csatlakozhasson"
diff --git a/libraries/eventformatter/impl/src/main/res/values-it/translations.xml b/libraries/eventformatter/impl/src/main/res/values-it/translations.xml
index 02e1cebef0d..1b7709dcb4b 100644
--- a/libraries/eventformatter/impl/src/main/res/values-it/translations.xml
+++ b/libraries/eventformatter/impl/src/main/res/values-it/translations.xml
@@ -28,8 +28,8 @@
"%1$s ti ha invitato"
"%1$s si è unito alla stanza"
"Ti sei unito alla stanza"
- "%1$s ha chiesto di unirsi"
- "%1$s ha permesso a %2$s di unirsi"
+ "%1$s ha richiesto di entrare"
+ "%1$s ha permesso a %2$s di entrare"
"Hai permesso a %1$s di partecipare"
"Hai richiesto di unirti"
"%1$s ha rifiutato la richiesta di unirsi di %2$s"
diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
index 7d21ed11389..4668d6db7df 100644
--- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
+++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
@@ -154,4 +154,18 @@ enum class FeatureFlags(
defaultValue = { true },
isFinished = false,
),
+ MediaGallery(
+ key = "feature.media_gallery",
+ title = "Allow user to open the media gallery",
+ description = null,
+ defaultValue = { true },
+ isFinished = false,
+ ),
+ EventCache(
+ key = "feature.event_cache",
+ title = "Use SDK Event cache",
+ description = "Warning: you must kill and restart the app for the change to take effect.",
+ defaultValue = { false },
+ isFinished = false,
+ ),
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CurrentUserMembership.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CurrentUserMembership.kt
index b1bc3648d78..01f43588a9d 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CurrentUserMembership.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/CurrentUserMembership.kt
@@ -12,4 +12,5 @@ enum class CurrentUserMembership {
JOINED,
LEFT,
KNOCKED,
+ BANNED,
}
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 989c301e928..8dbd78fab3c 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
@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
+import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
@@ -32,6 +33,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.io.Closeable
@@ -52,10 +54,17 @@ interface MatrixRoom : Closeable {
val activeMemberCount: Long
val joinedMemberCount: Long
+ val roomCoroutineScope: CoroutineScope
+
val roomInfoFlow: Flow
val roomTypingMembersFlow: Flow>
val identityStateChangesFlow: Flow>
+ /**
+ * The current knock requests in the room as a Flow.
+ */
+ val knockRequestsFlow: Flow>
+
/**
* A one-to-one is a room with exactly 2 members.
* See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules).
@@ -107,6 +116,11 @@ interface MatrixRoom : Closeable {
*/
suspend fun pinnedEventsTimeline(): Result
+ /**
+ * Create a new timeline for the media events of the room.
+ */
+ suspend fun mediaTimeline(): Result
+
fun destroy()
suspend fun subscribeToSync()
@@ -227,6 +241,11 @@ interface MatrixRoom : Closeable {
*/
suspend fun setUnreadFlag(isUnread: Boolean): Result
+ /**
+ * Clear the event cache storage for the current room.
+ */
+ suspend fun clearEventCacheStorage(): Result
+
/**
* Share a location message in the room.
*
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 6105a59c384..825ff85e969 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
@@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
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.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
@@ -27,6 +28,7 @@ data class MatrixRoomInfo(
val avatarUrl: String?,
val isDirect: Boolean,
val isPublic: Boolean,
+ val joinRule: JoinRule?,
val isSpace: Boolean,
val isTombstoned: Boolean,
val isFavorite: Boolean,
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt
new file mode 100644
index 00000000000..2ba4893ec80
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.room.join
+
+import io.element.android.libraries.matrix.api.core.RoomId
+
+sealed interface AllowRule {
+ data class RoomMembership(val roomId: RoomId) : AllowRule
+ data class Custom(val json: String) : AllowRule
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt
new file mode 100644
index 00000000000..b597fcf781a
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.room.join
+
+sealed interface JoinRule {
+ data object Public : JoinRule
+ data object Private : JoinRule
+ data object Knock : JoinRule
+ data object Invite : JoinRule
+ data class Restricted(val rules: List) : JoinRule
+ data class KnockRestricted(val rules: List) : JoinRule
+ data class Custom(val value: String) : JoinRule
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt
new file mode 100644
index 00000000000..e7a04882453
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.api.room.knock
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UserId
+
+interface KnockRequest {
+ val eventId: EventId
+ val userId: UserId
+ val displayName: String?
+ val avatarUrl: String?
+ val reason: String?
+ val timestamp: Long?
+ val isSeen: Boolean
+
+ suspend fun accept(): Result
+
+ suspend fun decline(reason: String?): Result
+
+ suspend fun declineAndBan(reason: String?): Result
+
+ suspend fun markAsSeen(): Result
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
index 682596a59d0..ba81a5780c7 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/MatrixRoomPowerLevels.kt
@@ -57,6 +57,13 @@ suspend fun MatrixRoom.canRedactOwn(): Result = canUserRedactOwn(sessio
*/
suspend fun MatrixRoom.canRedactOther(): Result = canUserRedactOther(sessionId)
+/**
+ * Shortcut for checking if current user can handle knock requests.
+ */
+suspend fun MatrixRoom.canHandleKnockRequests(): Result = runCatching {
+ canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow()
+}
+
/**
* Shortcut for calling [MatrixRoom.canUserPinUnpin] with our own user.
*/
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt
index d47e61099c5..e3a6a64d462 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/RoomListFilter.kt
@@ -7,6 +7,8 @@
package io.element.android.libraries.matrix.api.roomlist
+import io.element.android.libraries.core.extensions.withoutAccents
+
sealed interface RoomListFilter {
companion object {
/**
@@ -73,5 +75,7 @@ sealed interface RoomListFilter {
*/
data class NormalizedMatchRoomName(
val pattern: String
- ) : RoomListFilter
+ ) : RoomListFilter {
+ val normalizedPattern: String = pattern.withoutAccents()
+ }
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt
index 00f7a9a17ce..29f8997acea 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt
@@ -42,7 +42,8 @@ interface Timeline : AutoCloseable {
enum class Mode {
LIVE,
FOCUSED_ON_EVENT,
- PINNED_EVENTS
+ PINNED_EVENTS,
+ MEDIA,
}
val membershipChangeEventReceived: Flow
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/UtdCause.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/UtdCause.kt
index 51427c6cbae..ab39f49befc 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/UtdCause.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/UtdCause.kt
@@ -15,10 +15,17 @@ enum class UtdCause {
UnknownDevice,
/**
- * Expected utd because this is a device-historical message and
- * key storage is not setup or not configured correctly.
+ * We are missing the keys for this event, but it is a "device-historical" message and
+ * there is no key storage backup on the server, presumably because the user has turned it off.
*/
- HistoricalMessage,
+ HistoricalMessageAndBackupIsDisabled,
+
+ /**
+ * We are missing the keys for this event, but it is a "device-historical"
+ * message, and even though a key storage backup does exist, we can't use
+ * it because our device is unverified.
+ */
+ HistoricalMessageAndDeviceIsUnverified,
/**
* The key was withheld on purpose because your device is insecure and/or the
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
index 0ce1d2362b3..d5af4ff67f2 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt
@@ -109,6 +109,7 @@ class RustMatrixClientFactory @Inject constructor(
.addRootCertificates(userCertificatesProvider.provides())
.autoEnableBackups(true)
.autoEnableCrossSigning(true)
+ .useEventCachePersistentStorage(featureFlagService.isFeatureEnabled(FeatureFlags.EventCache))
.roomKeyRecipientStrategy(
strategy = if (featureFlagService.isFeatureEnabled(FeatureFlags.OnlySignedDeviceIsolationMode)) {
CollectStrategy.IdentityBasedStrategy
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTracker.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTracker.kt
index edbeb1d23db..aed1d7a0554 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTracker.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTracker.kt
@@ -27,7 +27,11 @@ class UtdTracker(
UtdCause.UNKNOWN_DEVICE -> {
Error.Name.ExpectedSentByInsecureDevice
}
- UtdCause.HISTORICAL_MESSAGE -> Error.Name.HistoricalMessage
+ UtdCause.HISTORICAL_MESSAGE_AND_BACKUP_IS_DISABLED,
+ UtdCause.HISTORICAL_MESSAGE_AND_DEVICE_IS_UNVERIFIED,
+ -> Error.Name.HistoricalMessage
+ UtdCause.WITHHELD_FOR_UNVERIFIED_OR_INSECURE_DEVICE -> Error.Name.RoomKeysWithheldForUnverifiedDevice
+ UtdCause.WITHHELD_BY_SENDER -> Error.Name.OlmKeysNotSentError
}
val event = Error(
context = null,
@@ -37,6 +41,10 @@ class UtdTracker(
timeToDecryptMillis = info.timeToDecryptMs?.toInt() ?: -1,
domain = Error.Domain.E2EE,
name = name,
+ eventLocalAgeMillis = info.eventLocalAgeMillis.toInt(),
+ userTrustsOwnIdentity = info.userTrustsOwnIdentity,
+ isFederated = info.ownHomeserver != info.senderHomeserver,
+ isMatrixDotOrg = info.ownHomeserver == "matrix.org",
)
analyticsService.capture(event)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
index cf48d68c703..c3a920dc696 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
@@ -22,6 +22,7 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
is ClientBuildException.SlidingSync -> AuthenticationException.Generic(message)
is ClientBuildException.WellKnownDeserializationException -> AuthenticationException.Generic(message)
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
+ is ClientBuildException.EventCache -> AuthenticationException.Generic(message)
}
else -> AuthenticationException.Generic(message)
}
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 607c316b257..029f6ae508a 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
@@ -15,6 +15,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.api.user.MatrixUser
+import io.element.android.libraries.matrix.impl.room.join.map
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
@@ -36,6 +37,7 @@ class MatrixRoomInfoMapper {
avatarUrl = it.avatarUrl,
isDirect = it.isDirect,
isPublic = it.isPublic,
+ joinRule = it.joinRule?.map(),
isSpace = it.isSpace,
isTombstoned = it.isTombstoned,
isFavorite = it.isFavourite,
@@ -67,6 +69,7 @@ fun RustMembership.map(): CurrentUserMembership = when (this) {
RustMembership.JOINED -> CurrentUserMembership.JOINED
RustMembership.LEFT -> CurrentUserMembership.LEFT
Membership.KNOCKED -> CurrentUserMembership.KNOCKED
+ RustMembership.BANNED -> CurrentUserMembership.BANNED
}
fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) {
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 c3057298fa5..609b6481c28 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
@@ -38,6 +38,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
+import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
@@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.impl.core.RustSendHandle
import io.element.android.libraries.matrix.impl.mapper.map
import io.element.android.libraries.matrix.impl.room.draft.into
+import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
@@ -74,10 +76,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
+import org.matrix.rustcomponents.sdk.DateDividerMode
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
+import org.matrix.rustcomponents.sdk.KnockRequestsListener
import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomInfoListener
import org.matrix.rustcomponents.sdk.RoomListItem
+import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
import org.matrix.rustcomponents.sdk.TypingNotificationsListener
import org.matrix.rustcomponents.sdk.UserPowerLevelUpdate
import org.matrix.rustcomponents.sdk.WidgetCapabilities
@@ -89,6 +94,7 @@ import uniffi.matrix_sdk.RoomPowerLevelChanges
import java.io.File
import kotlin.coroutines.cancellation.CancellationException
import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange
+import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest
import org.matrix.rustcomponents.sdk.Room as InnerRoom
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
@@ -155,13 +161,22 @@ class RustMatrixRoom(
})
}
+ override val knockRequestsFlow: Flow> = mxCallbackFlow {
+ innerRoom.subscribeToKnockRequests(object : KnockRequestsListener {
+ override fun call(joinRequests: List) {
+ val knockRequests = joinRequests.map { RustKnockRequest(it) }
+ channel.trySend(knockRequests)
+ }
+ })
+ }
+
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
// ...except getMember methods as it could quickly fill the roomDispatcher...
private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8)
- private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
+ override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
private val _syncUpdateFlow = MutableStateFlow(0L)
private val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher)
@@ -189,8 +204,8 @@ class RustMatrixRoom(
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
- override suspend fun timelineFocusedOnEvent(eventId: EventId): Result {
- return runCatching {
+ override suspend fun timelineFocusedOnEvent(eventId: EventId): Result = withContext(roomDispatcher) {
+ runCatching {
innerRoom.timelineFocusedOnEvent(
eventId = eventId.value,
numContextEvents = 50u,
@@ -207,8 +222,8 @@ class RustMatrixRoom(
}
}
- override suspend fun pinnedEventsTimeline(): Result {
- return runCatching {
+ override suspend fun pinnedEventsTimeline(): Result = withContext(roomDispatcher) {
+ runCatching {
innerRoom.pinnedEventsTimeline(
internalIdPrefix = "pinned_events",
maxEventsToLoad = 100u,
@@ -223,6 +238,27 @@ class RustMatrixRoom(
}
}
+ override suspend fun mediaTimeline(): Result = withContext(roomDispatcher) {
+ runCatching {
+ innerRoom.messageFilteredTimeline(
+ internalIdPrefix = "MediaGallery_",
+ allowedMessageTypes = listOf(
+ RoomMessageEventMessageType.FILE,
+ RoomMessageEventMessageType.IMAGE,
+ RoomMessageEventMessageType.VIDEO,
+ RoomMessageEventMessageType.AUDIO,
+ ),
+ dateDividerMode = DateDividerMode.MONTHLY,
+ ).let { inner ->
+ createTimeline(inner, mode = Timeline.Mode.MEDIA)
+ }
+ }.onFailure {
+ if (it is CancellationException) {
+ throw it
+ }
+ }
+ }
+
override fun destroy() {
roomCoroutineScope.cancel()
liveTimeline.close()
@@ -546,6 +582,12 @@ class RustMatrixRoom(
}
}
+ override suspend fun clearEventCacheStorage(): Result = withContext(roomDispatcher) {
+ runCatching {
+ innerRoom.clearEventCacheStorage()
+ }
+ }
+
override suspend fun kickUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) {
runCatching {
innerRoom.kickUser(userId.value, reason)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt
new file mode 100644
index 00000000000..86f37c5f20c
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.impl.room.join
+
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.room.join.AllowRule
+import org.matrix.rustcomponents.sdk.AllowRule as RustAllowRule
+
+fun RustAllowRule.map(): AllowRule {
+ return when (this) {
+ is RustAllowRule.RoomMembership -> AllowRule.RoomMembership(RoomId(roomId))
+ is RustAllowRule.Custom -> AllowRule.Custom(json)
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt
new file mode 100644
index 00000000000..f5c65c7283b
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.impl.room.join
+
+import io.element.android.libraries.matrix.api.room.join.JoinRule
+import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
+
+fun RustJoinRule.map(): JoinRule {
+ return when (this) {
+ RustJoinRule.Public -> JoinRule.Public
+ RustJoinRule.Private -> JoinRule.Private
+ RustJoinRule.Knock -> JoinRule.Knock
+ RustJoinRule.Invite -> JoinRule.Invite
+ is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() })
+ is RustJoinRule.Custom -> JoinRule.Custom(repr)
+ is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() })
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt
new file mode 100644
index 00000000000..9e12866c9c6
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.impl.room.knock
+
+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.room.knock.KnockRequest
+import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest
+
+class RustKnockRequest(
+ private val inner: InnerKnockRequest,
+) : KnockRequest {
+ override val eventId: EventId = EventId(inner.eventId)
+ override val userId: UserId = UserId(inner.userId)
+ override val displayName: String? = inner.displayName
+ override val avatarUrl: String? = inner.avatarUrl
+ override val reason: String? = inner.reason
+ override val timestamp: Long? = inner.timestamp?.toLong()
+ override val isSeen: Boolean = inner.isSeen
+
+ override suspend fun accept(): Result = runCatching {
+ inner.actions.accept()
+ }
+
+ override suspend fun decline(reason: String?): Result = runCatching {
+ inner.actions.decline(reason)
+ }
+
+ override suspend fun declineAndBan(reason: String?): Result = runCatching {
+ inner.actions.declineAndBan(reason)
+ }
+
+ override suspend fun markAsSeen(): Result = runCatching {
+ inner.actions.markAsSeen()
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt
index 88458d56bbf..41ef1d79a26 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilter.kt
@@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.roomlist
+import io.element.android.libraries.core.extensions.withoutAccents
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
@@ -30,7 +31,7 @@ val RoomListFilter.predicate
!roomSummary.isInvited() && (roomSummary.info.numUnreadNotifications > 0 || roomSummary.info.isMarkedUnread)
}
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
- roomSummary.info.name.orEmpty().contains(pattern, ignoreCase = true)
+ roomSummary.info.name?.withoutAccents().orEmpty().contains(normalizedPattern, ignoreCase = true)
}
RoomListFilter.Invite -> { roomSummary: RoomSummary ->
roomSummary.isInvited()
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
index 200f1289b42..c8e8b77b32b 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt
@@ -56,6 +56,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.launchIn
@@ -182,10 +183,10 @@ class RustTimeline(
Timeline.PaginationDirection.FORWARDS -> inner.focusedPaginateForwards(PAGINATION_SIZE.toUShort())
}
}.onFailure { error ->
- updatePaginationStatus(direction) { it.copy(isPaginating = false) }
if (error is TimelineException.CannotPaginate) {
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
} else {
+ updatePaginationStatus(direction) { it.copy(isPaginating = false) }
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
}
}.onSuccess { hasReachedEnd ->
@@ -211,13 +212,13 @@ class RustTimeline(
override val timelineItems: Flow> = combine(
_timelineItems,
- backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
- forwardPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
+ backPaginationStatus.filter { !it.isPaginating }.distinctUntilChanged(),
+ forwardPaginationStatus.filter { !it.isPaginating }.distinctUntilChanged(),
matrixRoom.roomInfoFlow.map { it.creator },
isTimelineInitialized,
) { timelineItems,
- hasMoreToLoadBackward,
- hasMoreToLoadForward,
+ backwardPaginationStatus,
+ forwardPaginationStatus,
roomCreator,
isTimelineInitialized ->
withContext(dispatcher) {
@@ -227,15 +228,15 @@ class RustTimeline(
items = items,
isDm = matrixRoom.isDm,
roomCreator = roomCreator,
- hasMoreToLoadBackwards = hasMoreToLoadBackward,
+ hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad,
)
}
.let { items ->
loadingIndicatorsPostProcessor.process(
items = items,
isTimelineInitialized = isTimelineInitialized,
- hasMoreToLoadBackward = hasMoreToLoadBackward,
- hasMoreToLoadForward = hasMoreToLoadForward
+ hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad,
+ hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad,
)
}
.let { items ->
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 7711c5af37e..3d13792eeb1 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
@@ -145,7 +145,10 @@ private fun RustUtdCause.map(): UtdCause {
RustUtdCause.VERIFICATION_VIOLATION -> UtdCause.VerificationViolation
RustUtdCause.UNSIGNED_DEVICE -> UtdCause.UnsignedDevice
RustUtdCause.UNKNOWN_DEVICE -> UtdCause.UnknownDevice
- RustUtdCause.HISTORICAL_MESSAGE -> UtdCause.HistoricalMessage
+ RustUtdCause.HISTORICAL_MESSAGE_AND_BACKUP_IS_DISABLED -> UtdCause.HistoricalMessageAndBackupIsDisabled
+ RustUtdCause.HISTORICAL_MESSAGE_AND_DEVICE_IS_UNVERIFIED -> UtdCause.HistoricalMessageAndDeviceIsUnverified
+ RustUtdCause.WITHHELD_FOR_UNVERIFIED_OR_INSECURE_DEVICE -> UtdCause.WithheldUnverifiedOrInsecureDevice
+ RustUtdCause.WITHHELD_BY_SENDER -> UtdCause.WithheldBySender
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt
index 841194abb41..befdf8903b9 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/virtual/VirtualTimelineItemMapper.kt
@@ -13,7 +13,7 @@ import org.matrix.rustcomponents.sdk.VirtualTimelineItem as RustVirtualTimelineI
class VirtualTimelineItemMapper {
fun map(virtualTimelineItem: RustVirtualTimelineItem): VirtualTimelineItem {
return when (virtualTimelineItem) {
- is RustVirtualTimelineItem.DayDivider -> VirtualTimelineItem.DayDivider(virtualTimelineItem.ts.toLong())
+ is RustVirtualTimelineItem.DateDivider -> VirtualTimelineItem.DayDivider(virtualTimelineItem.ts.toLong())
RustVirtualTimelineItem.ReadMarker -> VirtualTimelineItem.ReadMarker
}
}
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTrackerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTrackerTest.kt
index 994c9a339c2..deefe1a189d 100644
--- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTrackerTest.kt
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/analytics/UtdTrackerTest.kt
@@ -9,10 +9,10 @@ package io.element.android.libraries.matrix.impl.analytics
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Error
+import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUnableToDecryptInfo
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.services.analytics.test.FakeAnalyticsService
import org.junit.Test
-import org.matrix.rustcomponents.sdk.UnableToDecryptInfo
import uniffi.matrix_sdk_crypto.UtdCause
class UtdTrackerTest {
@@ -21,10 +21,11 @@ class UtdTrackerTest {
val fakeAnalyticsService = FakeAnalyticsService()
val sut = UtdTracker(fakeAnalyticsService)
sut.onUtd(
- UnableToDecryptInfo(
+ aRustUnableToDecryptInfo(
eventId = AN_EVENT_ID.value,
timeToDecryptMs = null,
cause = UtdCause.UNKNOWN,
+ eventLocalAgeMillis = 100L,
)
)
assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
@@ -34,7 +35,11 @@ class UtdTrackerTest {
cryptoSDK = Error.CryptoSDK.Rust,
timeToDecryptMillis = -1,
domain = Error.Domain.E2EE,
- name = Error.Name.OlmKeysNotSentError
+ name = Error.Name.OlmKeysNotSentError,
+ isFederated = false,
+ isMatrixDotOrg = false,
+ userTrustsOwnIdentity = false,
+ eventLocalAgeMillis = 100,
)
)
assertThat(fakeAnalyticsService.screenEvents).isEmpty()
@@ -46,7 +51,7 @@ class UtdTrackerTest {
val fakeAnalyticsService = FakeAnalyticsService()
val sut = UtdTracker(fakeAnalyticsService)
sut.onUtd(
- UnableToDecryptInfo(
+ aRustUnableToDecryptInfo(
eventId = AN_EVENT_ID.value,
timeToDecryptMs = 123.toULong(),
cause = UtdCause.UNKNOWN,
@@ -59,7 +64,11 @@ class UtdTrackerTest {
cryptoSDK = Error.CryptoSDK.Rust,
timeToDecryptMillis = 123,
domain = Error.Domain.E2EE,
- name = Error.Name.OlmKeysNotSentError
+ name = Error.Name.OlmKeysNotSentError,
+ isFederated = false,
+ isMatrixDotOrg = false,
+ userTrustsOwnIdentity = false,
+ eventLocalAgeMillis = 0,
)
)
assertThat(fakeAnalyticsService.screenEvents).isEmpty()
@@ -71,7 +80,7 @@ class UtdTrackerTest {
val fakeAnalyticsService = FakeAnalyticsService()
val sut = UtdTracker(fakeAnalyticsService)
sut.onUtd(
- UnableToDecryptInfo(
+ aRustUnableToDecryptInfo(
eventId = AN_EVENT_ID.value,
timeToDecryptMs = 123.toULong(),
cause = UtdCause.SENT_BEFORE_WE_JOINED,
@@ -84,7 +93,11 @@ class UtdTrackerTest {
cryptoSDK = Error.CryptoSDK.Rust,
timeToDecryptMillis = 123,
domain = Error.Domain.E2EE,
- name = Error.Name.ExpectedDueToMembership
+ name = Error.Name.ExpectedDueToMembership,
+ isFederated = false,
+ isMatrixDotOrg = false,
+ userTrustsOwnIdentity = false,
+ eventLocalAgeMillis = 0,
)
)
assertThat(fakeAnalyticsService.screenEvents).isEmpty()
@@ -96,7 +109,7 @@ class UtdTrackerTest {
val fakeAnalyticsService = FakeAnalyticsService()
val sut = UtdTracker(fakeAnalyticsService)
sut.onUtd(
- UnableToDecryptInfo(
+ aRustUnableToDecryptInfo(
eventId = AN_EVENT_ID.value,
timeToDecryptMs = 123.toULong(),
cause = UtdCause.UNSIGNED_DEVICE,
@@ -109,7 +122,11 @@ class UtdTrackerTest {
cryptoSDK = Error.CryptoSDK.Rust,
timeToDecryptMillis = 123,
domain = Error.Domain.E2EE,
- name = Error.Name.ExpectedSentByInsecureDevice
+ name = Error.Name.ExpectedSentByInsecureDevice,
+ isFederated = false,
+ isMatrixDotOrg = false,
+ userTrustsOwnIdentity = false,
+ eventLocalAgeMillis = 0,
)
)
}
@@ -119,7 +136,7 @@ class UtdTrackerTest {
val fakeAnalyticsService = FakeAnalyticsService()
val sut = UtdTracker(fakeAnalyticsService)
sut.onUtd(
- UnableToDecryptInfo(
+ aRustUnableToDecryptInfo(
eventId = AN_EVENT_ID.value,
timeToDecryptMs = 123.toULong(),
cause = UtdCause.VERIFICATION_VIOLATION,
@@ -132,7 +149,90 @@ class UtdTrackerTest {
cryptoSDK = Error.CryptoSDK.Rust,
timeToDecryptMillis = 123,
domain = Error.Domain.E2EE,
- name = Error.Name.ExpectedVerificationViolation
+ name = Error.Name.ExpectedVerificationViolation,
+ isFederated = false,
+ isMatrixDotOrg = false,
+ userTrustsOwnIdentity = false,
+ eventLocalAgeMillis = 0,
+ )
+ )
+ }
+
+ @Test
+ fun `when onUtd is called with different sender and receiver servers, the expected analytics Event is sent`() {
+ val fakeAnalyticsService = FakeAnalyticsService()
+ val sut = UtdTracker(fakeAnalyticsService)
+ sut.onUtd(
+ aRustUnableToDecryptInfo(
+ eventId = AN_EVENT_ID.value,
+ ownHomeserver = "example.com",
+ senderHomeserver = "matrix.org",
+ )
+ )
+ assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
+ Error(
+ context = null,
+ cryptoModule = Error.CryptoModule.Rust,
+ cryptoSDK = Error.CryptoSDK.Rust,
+ timeToDecryptMillis = -1,
+ domain = Error.Domain.E2EE,
+ name = Error.Name.OlmKeysNotSentError,
+ isFederated = true,
+ isMatrixDotOrg = false,
+ userTrustsOwnIdentity = false,
+ eventLocalAgeMillis = 0,
+ )
+ )
+ }
+
+ @Test
+ fun `when onUtd is called from a matrix-org user, the expected analytics Event is sent`() {
+ val fakeAnalyticsService = FakeAnalyticsService()
+ val sut = UtdTracker(fakeAnalyticsService)
+ sut.onUtd(
+ aRustUnableToDecryptInfo(
+ eventId = AN_EVENT_ID.value,
+ ownHomeserver = "matrix.org",
+ )
+ )
+ assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
+ Error(
+ context = null,
+ cryptoModule = Error.CryptoModule.Rust,
+ cryptoSDK = Error.CryptoSDK.Rust,
+ timeToDecryptMillis = -1,
+ domain = Error.Domain.E2EE,
+ name = Error.Name.OlmKeysNotSentError,
+ isFederated = true,
+ isMatrixDotOrg = true,
+ userTrustsOwnIdentity = false,
+ eventLocalAgeMillis = 0,
+ )
+ )
+ }
+
+ @Test
+ fun `when onUtd is called from a verified device, the expected analytics Event is sent`() {
+ val fakeAnalyticsService = FakeAnalyticsService()
+ val sut = UtdTracker(fakeAnalyticsService)
+ sut.onUtd(
+ aRustUnableToDecryptInfo(
+ eventId = AN_EVENT_ID.value,
+ userTrustsOwnIdentity = true,
+ )
+ )
+ assertThat(fakeAnalyticsService.capturedEvents).containsExactly(
+ Error(
+ context = null,
+ cryptoModule = Error.CryptoModule.Rust,
+ cryptoSDK = Error.CryptoSDK.Rust,
+ timeToDecryptMillis = -1,
+ domain = Error.Domain.E2EE,
+ name = Error.Name.OlmKeysNotSentError,
+ isFederated = false,
+ isMatrixDotOrg = false,
+ userTrustsOwnIdentity = true,
+ eventLocalAgeMillis = 0,
)
)
}
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt
index 81a1667f4ef..810adbfb3a4 100644
--- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationExceptionMappingTest.kt
@@ -52,6 +52,8 @@ class AuthenticationExceptionMappingTest {
.isException("WellKnown Deserialization")
assertThat(ClientBuildException.WellKnownLookupFailed("WellKnown Lookup Failed").mapAuthenticationException())
.isException("WellKnown Lookup Failed")
+ assertThat(ClientBuildException.EventCache("EventCache error").mapAuthenticationException())
+ .isException("EventCache error")
}
private inline fun ThrowableSubject.isException(message: String) {
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UnableToDecryptInfo.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UnableToDecryptInfo.kt
new file mode 100644
index 00000000000..775934716f9
--- /dev/null
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/UnableToDecryptInfo.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.impl.fixtures.factories
+
+import org.matrix.rustcomponents.sdk.UnableToDecryptInfo
+import uniffi.matrix_sdk_crypto.UtdCause
+
+internal fun aRustUnableToDecryptInfo(
+ eventId: String,
+ timeToDecryptMs: ULong? = null,
+ cause: UtdCause = UtdCause.UNKNOWN,
+ eventLocalAgeMillis: Long = 0L,
+ userTrustsOwnIdentity: Boolean = false,
+ senderHomeserver: String = "",
+ ownHomeserver: String = "",
+): UnableToDecryptInfo {
+ return UnableToDecryptInfo(
+ eventId = eventId,
+ timeToDecryptMs = timeToDecryptMs,
+ cause = cause,
+ eventLocalAgeMillis = eventLocalAgeMillis,
+ userTrustsOwnIdentity = userTrustsOwnIdentity,
+ senderHomeserver = senderHomeserver,
+ ownHomeserver = ownHomeserver,
+ )
+}
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt
index c22e7adb79b..a1af968c345 100644
--- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt
@@ -41,6 +41,7 @@ class FakeRustClientBuilder : ClientBuilder(NoPointer) {
override fun slidingSyncVersionBuilder(versionBuilder: SlidingSyncVersionBuilder) = this
override fun userAgent(userAgent: String) = this
override fun username(username: String) = this
+ override fun useEventCachePersistentStorage(value: Boolean) = this
override suspend fun buildWithQrCode(qrCodeData: QrCodeData, oidcConfiguration: OidcConfiguration, progressListener: QrLoginProgressListener): Client {
return FakeRustClient()
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt
index d2fa7148805..f277b26bae5 100644
--- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt
@@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
+import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo
@@ -31,6 +32,7 @@ import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toPersistentList
import org.junit.Test
import org.matrix.rustcomponents.sdk.Membership
+import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
class MatrixRoomInfoMapperTest {
@@ -47,6 +49,7 @@ class MatrixRoomInfoMapperTest {
isDirect = true,
isPublic = false,
isSpace = false,
+ joinRule = RustJoinRule.Invite,
isTombstoned = false,
isFavourite = false,
canonicalAlias = A_ROOM_ALIAS.value,
@@ -83,6 +86,7 @@ class MatrixRoomInfoMapperTest {
isSpace = false,
isTombstoned = false,
isFavorite = false,
+ joinRule = JoinRule.Invite,
canonicalAlias = A_ROOM_ALIAS,
alternativeAliases = listOf(A_ROOM_ALIAS).toImmutableList(),
currentUserMembership = CurrentUserMembership.JOINED,
@@ -125,6 +129,7 @@ class MatrixRoomInfoMapperTest {
avatarUrl = null,
isDirect = false,
isPublic = true,
+ joinRule = null,
isSpace = false,
isTombstoned = false,
isFavourite = true,
@@ -159,6 +164,7 @@ class MatrixRoomInfoMapperTest {
avatarUrl = null,
isDirect = false,
isPublic = true,
+ joinRule = null,
isSpace = false,
isTombstoned = false,
isFavorite = true,
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTest.kt
index abe7e17bbac..d057e4ecc3d 100644
--- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTest.kt
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFilterTest.kt
@@ -34,6 +34,9 @@ class RoomListFilterTest {
private val roomToSearch = aRoomSummary(
name = "Room to search"
)
+ private val roomWithAccent = aRoomSummary(
+ name = "Frédéric"
+ )
private val invitedRoom = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED
)
@@ -45,6 +48,7 @@ class RoomListFilterTest {
markedAsUnreadRoom,
unreadNotificationRoom,
roomToSearch,
+ roomWithAccent,
invitedRoom
)
@@ -69,7 +73,14 @@ class RoomListFilterTest {
@Test
fun `Room list filter group`() = runTest {
val filter = RoomListFilter.Category.Group
- assertThat(roomSummaries.filter(filter)).containsExactly(regularRoom, favoriteRoom, markedAsUnreadRoom, unreadNotificationRoom, roomToSearch)
+ assertThat(roomSummaries.filter(filter)).containsExactly(
+ regularRoom,
+ favoriteRoom,
+ markedAsUnreadRoom,
+ unreadNotificationRoom,
+ roomToSearch,
+ roomWithAccent,
+ )
}
@Test
@@ -96,6 +107,18 @@ class RoomListFilterTest {
assertThat(roomSummaries.filter(filter)).containsExactly(roomToSearch)
}
+ @Test
+ fun `Room list filter normalized match room name with accent`() = runTest {
+ val filter = RoomListFilter.NormalizedMatchRoomName("Fred")
+ assertThat(roomSummaries.filter(filter)).containsExactly(roomWithAccent)
+ }
+
+ @Test
+ fun `Room list filter normalized match room name with accent when searching with accent`() = runTest {
+ val filter = RoomListFilter.NormalizedMatchRoomName("Fréd")
+ assertThat(roomSummaries.filter(filter)).containsExactly(roomWithAccent)
+ }
+
@Test
fun `Room list filter all with one match`() = runTest {
val filter = RoomListFilter.all(
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 9974e367469..bea207eb8ad 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
@@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
+import io.element.android.libraries.matrix.api.room.knock.KnockRequest
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
@@ -48,12 +49,14 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.test.TestScope
import java.io.File
class FakeMatrixRoom(
@@ -72,6 +75,7 @@ class FakeMatrixRoom(
override val activeMemberCount: Long = 234L,
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
override val liveTimeline: Timeline = FakeTimeline(),
+ override val roomCoroutineScope: CoroutineScope = TestScope(),
private var roomPermalinkResult: () -> Result = { lambdaError() },
private var eventPermalinkResult: (EventId) -> Result = { lambdaError() },
private val sendCallNotificationIfNeededResult: () -> Result = { lambdaError() },
@@ -133,6 +137,7 @@ class FakeMatrixRoom(
private val getMembersResult: (Int) -> Result> = { lambdaError() },
private val timelineFocusedOnEventResult: (EventId) -> Result = { lambdaError() },
private val pinnedEventsTimelineResult: () -> Result = { lambdaError() },
+ private val mediaTimelineResult: () -> Result = { lambdaError() },
private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> },
private val saveComposerDraftLambda: (ComposerDraft) -> Result = { _: ComposerDraft -> Result.success(Unit) },
private val loadComposerDraftLambda: () -> Result = { Result.success(null) },
@@ -162,6 +167,13 @@ class FakeMatrixRoom(
_identityStateChangesFlow.tryEmit(identityStateChanges)
}
+ private val _knockRequestsFlow: MutableSharedFlow> = MutableSharedFlow(replay = 1)
+ override val knockRequestsFlow: Flow> = _knockRequestsFlow
+
+ fun emitKnockRequests(knockRequests: List) {
+ _knockRequestsFlow.tryEmit(knockRequests)
+ }
+
override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown)
override val roomNotificationSettingsStateFlow: MutableStateFlow =
@@ -203,6 +215,10 @@ class FakeMatrixRoom(
pinnedEventsTimelineResult()
}
+ override suspend fun mediaTimeline(): Result = simulateLongTask {
+ mediaTimelineResult()
+ }
+
override suspend fun subscribeToSync() {
subscribeToSyncLambda()
}
@@ -569,6 +585,10 @@ class FakeMatrixRoom(
fun givenRoomMembersState(state: MatrixRoomMembersState) {
membersStateFlow.value = state
}
+
+ override suspend fun clearEventCacheStorage(): Result {
+ return Result.success(Unit)
+ }
}
fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt
index 5aae1a32580..c05c24e7deb 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt
@@ -15,6 +15,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.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
+import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -33,6 +34,7 @@ fun aRoomInfo(
avatarUrl: String? = AN_AVATAR_URL,
isDirect: Boolean = false,
isPublic: Boolean = true,
+ joinRule: JoinRule? = JoinRule.Public,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
isFavorite: Boolean = false,
@@ -64,6 +66,7 @@ fun aRoomInfo(
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
+ joinRule = joinRule,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavorite = isFavorite,
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 39e13cb6198..cd612f5ec19 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
@@ -15,6 +15,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.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
+import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
@@ -46,6 +47,7 @@ fun aRoomSummary(
avatarUrl: String? = null,
isDirect: Boolean = false,
isPublic: Boolean = true,
+ joinRule: JoinRule? = JoinRule.Public,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
isFavorite: Boolean = false,
@@ -79,6 +81,7 @@ fun aRoomSummary(
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
+ joinRule = joinRule,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavorite = isFavorite,
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt
new file mode 100644
index 00000000000..416feae8b89
--- /dev/null
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.matrix.test.room.knock
+
+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.room.knock.KnockRequest
+import io.element.android.libraries.matrix.test.AN_AVATAR_URL
+import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.A_USER_ID
+import io.element.android.libraries.matrix.test.A_USER_NAME
+import io.element.android.tests.testutils.lambda.lambdaError
+import io.element.android.tests.testutils.simulateLongTask
+
+class FakeKnockRequest(
+ override val eventId: EventId = AN_EVENT_ID,
+ override val userId: UserId = A_USER_ID,
+ override val displayName: String? = A_USER_NAME,
+ override val avatarUrl: String? = AN_AVATAR_URL,
+ override val reason: String? = null,
+ override val timestamp: Long? = null,
+ override val isSeen: Boolean = false,
+ val acceptLambda: () -> Result = { lambdaError() },
+ val declineLambda: (String?) -> Result = { lambdaError() },
+ val declineAndBanLambda: (String?) -> Result = { lambdaError() },
+ val markAsSeenLambda: () -> Result = { lambdaError() },
+) : KnockRequest {
+ override suspend fun accept(): Result = simulateLongTask {
+ acceptLambda()
+ }
+
+ override suspend fun decline(reason: String?): Result = simulateLongTask {
+ declineLambda(reason)
+ }
+
+ override suspend fun declineAndBan(reason: String?): Result = simulateLongTask {
+ declineAndBanLambda(reason)
+ }
+
+ override suspend fun markAsSeen(): Result = simulateLongTask {
+ markAsSeenLambda()
+ }
+}
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt
index 81ae3e6b895..59bd1fa773d 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt
@@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
+import io.element.android.libraries.matrix.api.room.powerlevels.canHandleKnockRequests
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
import io.element.android.libraries.matrix.api.room.powerlevels.canKick
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
@@ -86,6 +87,13 @@ fun MatrixRoom.canBanAsState(updateKey: Long): State {
}
}
+@Composable
+fun MatrixRoom.canHandleKnockRequestsAsState(updateKey: Long): State {
+ return produceState(initialValue = false, key1 = updateKey) {
+ value = canHandleKnockRequests().getOrElse { false }
+ }
+}
+
@Composable
fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State {
return produceState(initialValue = 0, key1 = updateKey) {
diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt
index c013ddd5878..45fc226cd6f 100644
--- a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt
+++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt
@@ -15,7 +15,6 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -36,6 +35,7 @@ import kotlin.time.Duration.Companion.seconds
@SingleIn(RoomScope::class)
class DefaultMediaPlayer @Inject constructor(
private val player: SimplePlayer,
+ private val coroutineScope: CoroutineScope,
) : MediaPlayer {
private val listener = object : SimplePlayer.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
@@ -47,7 +47,7 @@ class DefaultMediaPlayer @Inject constructor(
)
}
if (isPlaying) {
- job = scope.launch { updateCurrentPosition() }
+ job = coroutineScope.launch { updateCurrentPosition() }
} else {
job?.cancel()
}
@@ -79,7 +79,6 @@ class DefaultMediaPlayer @Inject constructor(
player.addListener(listener)
}
- private val scope = CoroutineScope(Job() + Dispatchers.Main)
private var job: Job? = null
private val _state = MutableStateFlow(
@@ -102,7 +101,8 @@ class DefaultMediaPlayer @Inject constructor(
mimeType: String,
startPositionMs: Long,
): MediaPlayer.State {
- player.pause() // Must pause here otherwise if the player was playing it would keep on playing the new media item.
+ // Must pause here otherwise if the player was playing it would keep on playing the new media item.
+ player.pause()
player.clearMediaItems()
player.setMediaItem(
MediaItem.Builder()
@@ -129,11 +129,9 @@ class DefaultMediaPlayer @Inject constructor(
player.getCurrentMediaItem()?.let {
player.setMediaItem(it, 0)
player.prepare()
- player.play()
}
- } else {
- player.play()
}
+ player.play()
}
override fun pause() {
diff --git a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt
index 16242badb79..5262d65f3dd 100644
--- a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt
+++ b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt
@@ -7,12 +7,396 @@
package io.element.android.libraries.mediaplayer.impl
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.mediaplayer.api.MediaPlayer
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
import org.junit.Test
class DefaultMediaPlayerTest {
+ private val aMediaId = "mediaId"
+ private val aMediaItem = MediaItem.Builder().setMediaId(aMediaId).build()
+
+ @Test
+ fun `initial state`() = runTest {
+ val sut = createDefaultMediaPlayer()
+ sut.state.test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = null,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `start player will update the current position and pause it will stop`() = runTest {
+ val playLambda = lambdaRecorder { }
+ val pauseLambda = lambdaRecorder { }
+ val player = FakeSimplePlayer(
+ playLambda = playLambda,
+ pauseLambda = pauseLambda,
+ )
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.state.test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = null,
+ )
+ )
+ sut.play()
+ playLambda.assertions().isCalledOnce()
+ player.durationResult = 123L
+ player.simulateIsPlayingChanged(true)
+ val playingState = awaitItem()
+ assertThat(playingState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = true,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = 123,
+ )
+ )
+ player.currentPositionResult = 1L
+ assertThat(awaitItem()).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = true,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 1,
+ duration = 123,
+ )
+ )
+ player.currentPositionResult = 2L
+ assertThat(awaitItem()).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = true,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 2,
+ duration = 123,
+ )
+ )
+ player.pause()
+ pauseLambda.assertions().isCalledOnce()
+ player.simulateIsPlayingChanged(false)
+ assertThat(awaitItem()).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 2,
+ duration = 123,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `start player on ended playback will not invoke more methods if current media item is null`() = runTest {
+ val playLambda = lambdaRecorder { }
+ val getCurrentMediaItemLambda = lambdaRecorder { null }
+ val player = FakeSimplePlayer(
+ playLambda = playLambda,
+ getCurrentMediaItemLambda = getCurrentMediaItemLambda,
+ )
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.state.test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = null,
+ )
+ )
+ player.playbackStateResult = Player.STATE_ENDED
+ sut.play()
+ playLambda.assertions().isCalledOnce()
+ }
+ }
+
+ @Test
+ fun `start player on ended playback will invoke more methods if current media item is not null`() = runTest {
+ val playLambda = lambdaRecorder { }
+ val prepareLambda = lambdaRecorder { }
+ val getCurrentMediaItemLambda = lambdaRecorder { aMediaItem }
+ val setMediaItemLambda = lambdaRecorder { _, _ -> }
+ val player = FakeSimplePlayer(
+ playLambda = playLambda,
+ prepareLambda = prepareLambda,
+ setMediaItemLambda = setMediaItemLambda,
+ getCurrentMediaItemLambda = getCurrentMediaItemLambda,
+ )
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.state.test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = null,
+ )
+ )
+ player.playbackStateResult = Player.STATE_ENDED
+ sut.play()
+ setMediaItemLambda.assertions().isCalledOnce().with(
+ value(aMediaItem),
+ value(0L),
+ )
+ prepareLambda.assertions().isCalledOnce()
+ playLambda.assertions().isCalledOnce()
+ }
+ }
+
+ @Test
+ fun `pause player invokes pause on the embedded player`() = runTest {
+ val pauseLambda = lambdaRecorder { }
+ val player = FakeSimplePlayer(
+ pauseLambda = pauseLambda,
+ )
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.pause()
+ pauseLambda.assertions().isCalledOnce()
+ }
+
+ @Test
+ fun `close player invokes release on the embedded player`() = runTest {
+ val releaseLambda = lambdaRecorder { }
+ val player = FakeSimplePlayer(
+ releaseLambda = releaseLambda,
+ )
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.close()
+ releaseLambda.assertions().isCalledOnce()
+ }
+
@Test
- fun `default test`() = runTest {
- // TODO
+ fun `seekTo invokes release on the embedded player`() = runTest {
+ val seekToLambda = lambdaRecorder { }
+ val player = FakeSimplePlayer(
+ seekToLambda = seekToLambda,
+ )
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.state.test {
+ awaitItem()
+ player.currentPositionResult = 33L
+ sut.seekTo(33L)
+ seekToLambda.assertions().isCalledOnce().with(value(33L))
+ val finalState = awaitItem()
+ assertThat(finalState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 33L,
+ duration = null,
+ )
+ )
+ }
}
+
+ @Test
+ fun `onPlaybackStateChanged update the state`() = runTest {
+ val player = FakeSimplePlayer()
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.state.test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = null,
+ )
+ )
+ player.currentPositionResult = 44
+ player.durationResult = 123L
+ player.simulatePlaybackStateChanged(Player.STATE_READY)
+ val readyState = awaitItem()
+ assertThat(readyState).isEqualTo(
+ MediaPlayer.State(
+ isReady = true,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 44,
+ duration = 123,
+ )
+ )
+ player.simulatePlaybackStateChanged(Player.STATE_ENDED)
+ val endedState = awaitItem()
+ assertThat(endedState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = true,
+ mediaId = null,
+ currentPosition = 44,
+ duration = 123,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `setMedia with timeout error`() = runTest {
+ val pauseLambda = lambdaRecorder { }
+ val clearMediaItemsLambda = lambdaRecorder { }
+ val setMediaItemLambda = lambdaRecorder { _, _ -> }
+ val prepareLambda = lambdaRecorder { }
+ val player = FakeSimplePlayer(
+ pauseLambda = pauseLambda,
+ clearMediaItemsLambda = clearMediaItemsLambda,
+ setMediaItemLambda = setMediaItemLambda,
+ prepareLambda = prepareLambda,
+ )
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.state.test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = null,
+ )
+ )
+ val result = runCatching {
+ sut.setMedia("uri", "mediaId", "mimeType", 12)
+ }
+ pauseLambda.assertions().isCalledOnce()
+ clearMediaItemsLambda.assertions().isCalledOnce()
+ setMediaItemLambda.assertions().isCalledOnce().with(
+ value(MediaItem.Builder().setUri("uri").setMediaId("mediaId").setMimeType("mimeType").build()),
+ value(12L),
+ )
+ prepareLambda.assertions().isCalledOnce()
+ assertThat(result.isFailure).isTrue()
+ assertThrows(TimeoutCancellationException::class.java) {
+ result.getOrThrow()
+ }
+ }
+ }
+
+ @Test
+ fun `setMedia success`() = runTest {
+ var player: FakeSimplePlayer? = null
+ val pauseLambda = lambdaRecorder { }
+ val clearMediaItemsLambda = lambdaRecorder { }
+ val setMediaItemLambda = lambdaRecorder { _, _ -> }
+ val prepareLambda = lambdaRecorder {
+ player?.simulatePlaybackStateChanged(Player.STATE_READY)
+ player?.simulateMediaItemTransition(aMediaItem)
+ }
+ player = FakeSimplePlayer(
+ pauseLambda = pauseLambda,
+ clearMediaItemsLambda = clearMediaItemsLambda,
+ setMediaItemLambda = setMediaItemLambda,
+ prepareLambda = prepareLambda,
+ )
+ val sut = createDefaultMediaPlayer(
+ simplePlayer = player,
+ )
+ sut.state.test {
+ val initialState = awaitItem()
+ assertThat(initialState).isEqualTo(
+ MediaPlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = null,
+ )
+ )
+ val state = sut.setMedia("uri", "mediaId", "mimeType", 12)
+ pauseLambda.assertions().isCalledOnce()
+ clearMediaItemsLambda.assertions().isCalledOnce()
+ setMediaItemLambda.assertions().isCalledOnce().with(
+ value(MediaItem.Builder().setUri("uri").setMediaId("mediaId").setMimeType("mimeType").build()),
+ value(12L),
+ )
+ prepareLambda.assertions().isCalledOnce()
+
+ val finalState = MediaPlayer.State(
+ isReady = true,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = "mediaId",
+ currentPosition = 0,
+ duration = 0,
+ )
+ assertThat(awaitItem()).isEqualTo(
+ MediaPlayer.State(
+ isReady = true,
+ isPlaying = false,
+ isEnded = false,
+ mediaId = null,
+ currentPosition = 0,
+ duration = 0,
+ )
+ )
+ assertThat(awaitItem()).isEqualTo(finalState)
+ assertThat(state).isEqualTo(finalState)
+ }
+ }
+
+ private fun TestScope.createDefaultMediaPlayer(
+ simplePlayer: SimplePlayer = FakeSimplePlayer(),
+ ): DefaultMediaPlayer = DefaultMediaPlayer(
+ simplePlayer,
+ backgroundScope,
+ )
}
diff --git a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt
new file mode 100644
index 00000000000..d981fa77963
--- /dev/null
+++ b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/FakeSimplePlayer.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaplayer.impl
+
+import androidx.media3.common.MediaItem
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeSimplePlayer(
+ private val clearMediaItemsLambda: () -> Unit = { lambdaError() },
+ private val setMediaItemLambda: (MediaItem, Long) -> Unit = { _, _ -> lambdaError() },
+ private val getCurrentMediaItemLambda: () -> MediaItem? = { lambdaError() },
+ private val prepareLambda: () -> Unit = { lambdaError() },
+ private val playLambda: () -> Unit = { lambdaError() },
+ private val pauseLambda: () -> Unit = { lambdaError() },
+ private val seekToLambda: (Long) -> Unit = { lambdaError() },
+ private val releaseLambda: () -> Unit = { lambdaError() },
+) : SimplePlayer {
+ private val listeners = mutableListOf()
+ override fun addListener(listener: SimplePlayer.Listener) {
+ listeners.add(listener)
+ }
+
+ var currentPositionResult: Long = 0
+ override val currentPosition: Long get() = currentPositionResult
+ var playbackStateResult: Int = 0
+ override val playbackState: Int get() = playbackStateResult
+ var durationResult: Long = 0
+ override val duration: Long get() = durationResult
+
+ override fun clearMediaItems() = clearMediaItemsLambda()
+ override fun setMediaItem(mediaItem: MediaItem, startPositionMs: Long) {
+ setMediaItemLambda(mediaItem, startPositionMs)
+ }
+
+ override fun getCurrentMediaItem(): MediaItem? = getCurrentMediaItemLambda()
+ override fun prepare() = prepareLambda()
+ override fun play() = playLambda()
+ override fun pause() = pauseLambda()
+ override fun seekTo(positionMs: Long) = seekToLambda(positionMs)
+ override fun release() = releaseLambda()
+
+ fun simulateIsPlayingChanged(isPlaying: Boolean) {
+ listeners.forEach { it.onIsPlayingChanged(isPlaying) }
+ }
+
+ fun simulateMediaItemTransition(mediaItem: MediaItem?) {
+ listeners.forEach { it.onMediaItemTransition(mediaItem) }
+ }
+
+ fun simulatePlaybackStateChanged(playbackState: Int) {
+ listeners.forEach { it.onPlaybackStateChanged(playbackState) }
+ }
+}
diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt
new file mode 100644
index 00000000000..a26bb189156
--- /dev/null
+++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaGalleryEntryPoint.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.api
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.matrix.api.core.EventId
+
+interface MediaGalleryEntryPoint : FeatureEntryPoint {
+ fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
+
+ interface NodeBuilder {
+ fun callback(callback: Callback): NodeBuilder
+ fun build(): Node
+ }
+
+ interface Callback : Plugin {
+ fun onBackClick()
+ fun onViewInTimeline(eventId: EventId)
+ }
+}
diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt
index 5c317b1d6a4..7f2e823b1e1 100644
--- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt
+++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt
@@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.api
import android.os.Parcelable
import io.element.android.libraries.core.mimetype.MimeTypes
+import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -18,73 +19,130 @@ data class MediaInfo(
val mimeType: String,
val formattedFileSize: String,
val fileExtension: String,
+ val senderId: UserId?,
val senderName: String?,
+ val senderAvatar: String?,
val dateSent: String?,
+ val dateSentFull: String?,
+ val waveform: List?,
) : Parcelable
fun anImageMediaInfo(
+ senderId: UserId? = UserId("@alice:server.org"),
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
+ dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "an image file.jpg",
caption = caption,
mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB",
fileExtension = "jpg",
+ senderId = senderId,
senderName = senderName,
+ senderAvatar = null,
dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
)
fun aVideoMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
+ dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "a video file.mp4",
caption = caption,
mimeType = MimeTypes.Mp4,
formattedFileSize = "14MB",
fileExtension = "mp4",
+ senderId = UserId("@alice:server.org"),
senderName = senderName,
+ senderAvatar = null,
dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
)
fun aPdfMediaInfo(
+ filename: String = "a pdf file.pdf",
+ caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
+ dateSentFull: String? = null,
): MediaInfo = MediaInfo(
- filename = "a pdf file.pdf",
- caption = null,
+ filename = filename,
+ caption = caption,
mimeType = MimeTypes.Pdf,
formattedFileSize = "23MB",
fileExtension = "pdf",
+ senderId = UserId("@alice:server.org"),
senderName = senderName,
+ senderAvatar = null,
dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
)
fun anApkMediaInfo(
+ senderId: UserId? = UserId("@alice:server.org"),
senderName: String? = null,
dateSent: String? = null,
+ dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "an apk file.apk",
caption = null,
mimeType = MimeTypes.Apk,
formattedFileSize = "50MB",
fileExtension = "apk",
+ senderId = senderId,
senderName = senderName,
+ senderAvatar = null,
dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
)
fun anAudioMediaInfo(
+ filename: String = "an audio file.mp3",
+ caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
+ dateSentFull: String? = null,
+ waveForm: List? = null,
): MediaInfo = MediaInfo(
- filename = "an audio file.mp3",
- caption = null,
+ filename = filename,
+ caption = caption,
mimeType = MimeTypes.Mp3,
formattedFileSize = "7MB",
fileExtension = "mp3",
+ senderId = UserId("@alice:server.org"),
+ senderName = senderName,
+ senderAvatar = null,
+ dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = waveForm,
+)
+
+fun aVoiceMediaInfo(
+ filename: String = "a voice file.ogg",
+ caption: String? = null,
+ senderName: String? = null,
+ dateSent: String? = null,
+ dateSentFull: String? = null,
+ waveForm: List? = null,
+): MediaInfo = MediaInfo(
+ filename = filename,
+ caption = caption,
+ mimeType = MimeTypes.Ogg,
+ formattedFileSize = "3MB",
+ fileExtension = "ogg",
+ senderId = UserId("@alice:server.org"),
senderName = senderName,
+ senderAvatar = null,
dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = waveForm,
)
diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt
index fb5ee5dece7..598d7997234 100644
--- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt
+++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaViewerEntryPoint.kt
@@ -12,6 +12,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.EventId
import io.element.android.libraries.matrix.api.media.MediaSource
interface MediaViewerEntryPoint : FeatureEntryPoint {
@@ -26,13 +27,14 @@ interface MediaViewerEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onDone()
+ fun onViewInTimeline(eventId: EventId)
}
data class Params(
+ val eventId: EventId?,
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
- val canDownload: Boolean,
- val canShare: Boolean,
+ val canShowInfo: Boolean,
) : NodeInputs
}
diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts
index 5ebc252343d..395b57df387 100644
--- a/libraries/mediaviewer/impl/build.gradle.kts
+++ b/libraries/mediaviewer/impl/build.gradle.kts
@@ -39,9 +39,12 @@ dependencies {
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.di)
implementation(projects.libraries.designsystem)
+ implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.uiStrings)
+ implementation(projects.libraries.voiceplayer.api)
+ implementation(projects.services.toolbox.api)
api(projects.libraries.mediaviewer.api)
implementation(projects.libraries.androidutils)
@@ -49,8 +52,11 @@ dependencies {
implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api)
+ testImplementation(projects.libraries.dateformatter.test)
+ testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaviewer.test)
+ testImplementation(projects.services.toolbox.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPoint.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPoint.kt
new file mode 100644
index 00000000000..5d4fd8b297e
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaGalleryEntryPoint.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl
+
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
+import io.element.android.libraries.mediaviewer.impl.gallery.root.MediaGalleryRootNode
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultMediaGalleryEntryPoint @Inject constructor() : MediaGalleryEntryPoint {
+ override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): MediaGalleryEntryPoint.NodeBuilder {
+ val plugins = ArrayList()
+
+ return object : MediaGalleryEntryPoint.NodeBuilder {
+ override fun callback(callback: MediaGalleryEntryPoint.Callback): MediaGalleryEntryPoint.NodeBuilder {
+ plugins += callback
+ return this
+ }
+
+ override fun build(): Node {
+ return parentNode.createNode(buildContext, plugins)
+ }
+ }
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt
index 86d7bca722b..19ac8718d56 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt
@@ -14,6 +14,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
+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.MediaInfo
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
@@ -41,19 +42,23 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
val mimeType = MimeTypes.Images
return params(
MediaViewerEntryPoint.Params(
+ eventId = null,
mediaInfo = MediaInfo(
filename = filename,
caption = null,
mimeType = mimeType,
formattedFileSize = "",
fileExtension = "",
+ senderId = UserId("@dummy:server.org"),
senderName = null,
+ senderAvatar = null,
dateSent = null,
+ dateSentFull = null,
+ waveform = null,
),
mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null,
- canDownload = false,
- canShare = false,
+ canShowInfo = false,
)
)
}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetState.kt
new file mode 100644
index 00000000000..c55e3c22958
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.details
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+
+sealed interface MediaBottomSheetState {
+ data object Hidden : MediaBottomSheetState
+
+ data class MediaDeleteConfirmationState(
+ val eventId: EventId,
+ val mediaInfo: MediaInfo,
+ val thumbnailSource: MediaSource?,
+ ) : MediaBottomSheetState
+
+ data class MediaDetailsBottomSheetState(
+ val eventId: EventId?,
+ val canDelete: Boolean,
+ val mediaInfo: MediaInfo,
+ val thumbnailSource: MediaSource?,
+ ) : MediaBottomSheetState
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt
new file mode 100644
index 00000000000..b8e0075504c
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.details
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.designsystem.components.BigIcon
+import io.element.android.libraries.designsystem.components.PageTitle
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
+import io.element.android.libraries.designsystem.theme.components.TextButton
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.ui.media.MediaRequestData
+import io.element.android.libraries.mediaviewer.impl.R
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MediaDeleteConfirmationBottomSheet(
+ state: MediaBottomSheetState.MediaDeleteConfirmationState,
+ onDelete: (EventId) -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ ModalBottomSheet(
+ modifier = modifier,
+ onDismissRequest = onDismiss,
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
+ PageTitle(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp, horizontal = 8.dp),
+ title = stringResource(R.string.screen_media_browser_delete_confirmation_title),
+ iconStyle = BigIcon.Style.Default(CompoundIcons.Delete(), useCriticalTint = true),
+ subtitle = stringResource(R.string.screen_media_browser_delete_confirmation_subtitle),
+ )
+ MediaRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ state = state,
+ )
+ Button(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 40.dp),
+ text = stringResource(CommonStrings.action_remove),
+ onClick = {
+ onDelete(state.eventId)
+ },
+ destructive = true,
+ )
+ TextButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ text = stringResource(CommonStrings.action_cancel),
+ onClick = {
+ onDismiss()
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun MediaRow(
+ state: MediaBottomSheetState.MediaDeleteConfirmationState,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(40.dp),
+ ) {
+ if (state.thumbnailSource == null) {
+ BigIcon(
+ style = BigIcon.Style.Default(CompoundIcons.Attachment()),
+ )
+ } else {
+ AsyncImage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White),
+ model = MediaRequestData(state.thumbnailSource, MediaRequestData.Kind.Thumbnail(100)),
+ contentScale = ContentScale.Crop,
+ alignment = Alignment.Center,
+ contentDescription = null,
+ )
+ }
+ }
+ Column(
+ modifier = Modifier
+ .padding(start = 12.dp)
+ .weight(1f),
+ ) {
+ // Name
+ Text(
+ modifier = Modifier.clipToBounds(),
+ text = state.mediaInfo.filename,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ )
+ // Info
+ Text(
+ text = state.mediaInfo.mimeType + " - " + state.mediaInfo.formattedFileSize,
+ color = MaterialTheme.colorScheme.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodySmRegular,
+ )
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun MediaDeleteConfirmationBottomSheetPreview() = ElementPreview {
+ MediaDeleteConfirmationBottomSheet(
+ state = aMediaDeleteConfirmationState(),
+ onDelete = {},
+ onDismiss = {},
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt
new file mode 100644
index 00000000000..74d47797a28
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.details
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
+import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.components.list.ListItemContent
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
+import io.element.android.libraries.designsystem.theme.components.IconSource
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.ListItemStyle
+import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+import io.element.android.libraries.mediaviewer.impl.R
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MediaDetailsBottomSheet(
+ state: MediaBottomSheetState.MediaDetailsBottomSheetState,
+ onViewInTimeline: (EventId) -> Unit,
+ onShare: (EventId) -> Unit,
+ onDownload: (EventId) -> Unit,
+ onDelete: (EventId) -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ ModalBottomSheet(
+ modifier = modifier,
+ onDismissRequest = onDismiss,
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(24.dp),
+ ) {
+ Section(
+ title = stringResource(R.string.screen_media_details_uploaded_by),
+ ) {
+ SenderRow(
+ mediaInfo = state.mediaInfo,
+ )
+ }
+ SectionText(
+ title = stringResource(R.string.screen_media_details_uploaded_on),
+ text = state.mediaInfo.dateSentFull.orEmpty(),
+ )
+ SectionText(
+ title = stringResource(R.string.screen_media_details_filename),
+ text = state.mediaInfo.filename,
+ )
+ SectionText(
+ title = stringResource(R.string.screen_media_details_file_format),
+ text = state.mediaInfo.mimeType + " - " + state.mediaInfo.formattedFileSize,
+ )
+ if (state.eventId != null) {
+ Column {
+ HorizontalDivider()
+ ListItem(
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VisibilityOn())),
+ headlineContent = { Text(stringResource(CommonStrings.action_view_in_timeline)) },
+ style = ListItemStyle.Primary,
+ onClick = {
+ onViewInTimeline(state.eventId)
+ }
+ )
+ ListItem(
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ShareAndroid())),
+ headlineContent = { Text(stringResource(CommonStrings.action_share)) },
+ style = ListItemStyle.Primary,
+ onClick = {
+ onShare(state.eventId)
+ }
+ )
+ ListItem(
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Download())),
+ headlineContent = { Text(stringResource(CommonStrings.action_save)) },
+ style = ListItemStyle.Primary,
+ onClick = {
+ onDownload(state.eventId)
+ }
+ )
+ if (state.canDelete) {
+ HorizontalDivider()
+ ListItem(
+ leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())),
+ headlineContent = { Text(stringResource(CommonStrings.action_remove)) },
+ style = ListItemStyle.Destructive,
+ onClick = {
+ onDelete(state.eventId)
+ }
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SenderRow(
+ mediaInfo: MediaInfo,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val id = mediaInfo.senderId?.value ?: "@Alice:domain"
+ Avatar(
+ AvatarData(
+ id = id,
+ name = mediaInfo.senderName,
+ url = mediaInfo.senderAvatar,
+ size = AvatarSize.MediaSender,
+ )
+ )
+ Column(
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .weight(1f),
+ ) {
+ // Name
+ val avatarColors = AvatarColorsProvider.provide(id)
+ Text(
+ modifier = Modifier.clipToBounds(),
+ text = mediaInfo.senderName.orEmpty(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = avatarColors.foreground,
+ style = ElementTheme.typography.fontBodyMdMedium,
+ )
+ // Id
+ Text(
+ text = mediaInfo.senderId?.value.orEmpty(),
+ color = MaterialTheme.colorScheme.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ )
+ }
+ }
+}
+
+@Composable
+private fun Section(
+ title: String,
+ content: @Composable () -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ text = title.uppercase(),
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ content()
+ }
+}
+
+@Composable
+private fun SectionText(
+ title: String,
+ text: String,
+) {
+ Section(title = title) {
+ Text(
+ text = text,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textPrimary,
+ )
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun MediaDetailsBottomSheetPreview() = ElementPreview {
+ MediaDetailsBottomSheet(
+ state = aMediaDetailsBottomSheetState(),
+ onViewInTimeline = {},
+ onShare = {},
+ onDownload = {},
+ onDelete = {},
+ onDismiss = {},
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt
new file mode 100644
index 00000000000..25cab1429f4
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.details
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
+
+fun aMediaDetailsBottomSheetState(
+ dateSentFull: String = "December 6, 2024 at 12:59",
+ canDelete: Boolean = true,
+): MediaBottomSheetState.MediaDetailsBottomSheetState {
+ return MediaBottomSheetState.MediaDetailsBottomSheetState(
+ eventId = EventId("\$eventId"),
+ canDelete = canDelete,
+ mediaInfo = anImageMediaInfo(
+ senderName = "Alice",
+ dateSentFull = dateSentFull,
+ ),
+ thumbnailSource = null,
+ )
+}
+
+fun aMediaDeleteConfirmationState(): MediaBottomSheetState.MediaDeleteConfirmationState {
+ return MediaBottomSheetState.MediaDeleteConfirmationState(
+ eventId = EventId("\$eventId"),
+ mediaInfo = anImageMediaInfo(
+ senderName = "Alice",
+ ),
+ thumbnailSource = null,
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt
new file mode 100644
index 00000000000..b039cef4e82
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import io.element.android.libraries.dateformatter.api.toHumanReadableDuration
+import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
+import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
+import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
+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.LegacyCallInviteContent
+import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
+import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
+import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
+import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
+import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
+import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
+import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
+import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
+import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
+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.api.timeline.item.event.getAvatarUrl
+import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
+import kotlinx.collections.immutable.persistentListOf
+import timber.log.Timber
+import javax.inject.Inject
+
+class EventItemFactory @Inject constructor(
+ private val fileSizeFormatter: FileSizeFormatter,
+ private val fileExtensionExtractor: FileExtensionExtractor,
+ private val dateFormatter: DateFormatter,
+) {
+ fun create(
+ currentTimelineItem: MatrixTimelineItem.Event,
+ ): MediaItem.Event? {
+ val event = currentTimelineItem.event
+ val dateSent = dateFormatter.format(
+ currentTimelineItem.event.timestamp,
+ mode = DateFormatterMode.Day,
+ )
+ val dateSentFull = dateFormatter.format(
+ timestamp = currentTimelineItem.event.timestamp,
+ mode = DateFormatterMode.Full,
+ )
+ return when (val content = event.content) {
+ CallNotifyContent,
+ is FailedToParseMessageLikeContent,
+ is FailedToParseStateContent,
+ LegacyCallInviteContent,
+ is PollContent,
+ is ProfileChangeContent,
+ RedactedContent,
+ is RoomMembershipContent,
+ is StateContent,
+ is StickerContent,
+ is UnableToDecryptContent,
+ UnknownContent -> {
+ Timber.w("Should not happen: ${content.javaClass.simpleName}")
+ null
+ }
+ is MessageContent -> {
+ when (val type = content.type) {
+ is EmoteMessageType,
+ is NoticeMessageType,
+ is OtherMessageType,
+ is LocationMessageType,
+ is TextMessageType -> {
+ Timber.w("Should not happen: ${content.type}")
+ null
+ }
+ is AudioMessageType -> MediaItem.Audio(
+ id = currentTimelineItem.uniqueId,
+ eventId = currentTimelineItem.eventId,
+ mediaInfo = MediaInfo(
+ filename = type.filename,
+ caption = type.caption,
+ mimeType = type.info?.mimetype.orEmpty(),
+ formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
+ fileExtension = fileExtensionExtractor.extractFromName(type.filename),
+ senderId = event.sender,
+ senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
+ senderAvatar = event.senderProfile.getAvatarUrl(),
+ dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
+ ),
+ mediaSource = type.source,
+ )
+ is FileMessageType -> MediaItem.File(
+ id = currentTimelineItem.uniqueId,
+ eventId = currentTimelineItem.eventId,
+ mediaInfo = MediaInfo(
+ filename = type.filename,
+ caption = type.caption,
+ mimeType = type.info?.mimetype.orEmpty(),
+ formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
+ fileExtension = fileExtensionExtractor.extractFromName(type.filename),
+ senderId = event.sender,
+ senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
+ senderAvatar = event.senderProfile.getAvatarUrl(),
+ dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
+ ),
+ mediaSource = type.source,
+ )
+ is ImageMessageType -> MediaItem.Image(
+ id = currentTimelineItem.uniqueId,
+ eventId = currentTimelineItem.eventId,
+ mediaInfo = MediaInfo(
+ filename = type.filename,
+ caption = type.caption,
+ mimeType = type.info?.mimetype.orEmpty(),
+ formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
+ fileExtension = fileExtensionExtractor.extractFromName(type.filename),
+ senderId = event.sender,
+ senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
+ senderAvatar = event.senderProfile.getAvatarUrl(),
+ dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
+ ),
+ mediaSource = type.source,
+ thumbnailSource = null,
+ )
+ is StickerMessageType -> MediaItem.Image(
+ id = currentTimelineItem.uniqueId,
+ eventId = currentTimelineItem.eventId,
+ mediaInfo = MediaInfo(
+ filename = type.filename,
+ caption = type.caption,
+ mimeType = type.info?.mimetype.orEmpty(),
+ formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
+ fileExtension = fileExtensionExtractor.extractFromName(type.filename),
+ senderId = event.sender,
+ senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
+ senderAvatar = event.senderProfile.getAvatarUrl(),
+ dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
+ ),
+ mediaSource = type.source,
+ thumbnailSource = null,
+ )
+ is VideoMessageType -> MediaItem.Video(
+ id = currentTimelineItem.uniqueId,
+ eventId = currentTimelineItem.eventId,
+ mediaInfo = MediaInfo(
+ filename = type.filename,
+ caption = type.caption,
+ mimeType = type.info?.mimetype.orEmpty(),
+ formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
+ fileExtension = fileExtensionExtractor.extractFromName(type.filename),
+ senderId = event.sender,
+ senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
+ senderAvatar = event.senderProfile.getAvatarUrl(),
+ dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = null,
+ ),
+ mediaSource = type.source,
+ thumbnailSource = type.info?.thumbnailSource,
+ duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
+ )
+ is VoiceMessageType -> MediaItem.Voice(
+ id = currentTimelineItem.uniqueId,
+ eventId = currentTimelineItem.eventId,
+ mediaInfo = MediaInfo(
+ filename = type.filename,
+ caption = type.caption,
+ mimeType = type.info?.mimetype.orEmpty(),
+ formattedFileSize = type.info?.size?.let { fileSizeFormatter.format(it) }.orEmpty(),
+ fileExtension = fileExtensionExtractor.extractFromName(type.filename),
+ senderId = event.sender,
+ senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
+ senderAvatar = event.senderProfile.getAvatarUrl(),
+ dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = type.details?.waveform.orEmpty(),
+ ),
+ mediaSource = type.source,
+ duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
+ waveform = type.details?.waveform ?: persistentListOf(),
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt
new file mode 100644
index 00000000000..e4199e6d5ea
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryEvents.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+
+sealed interface MediaGalleryEvents {
+ data class ChangeMode(val mode: MediaGalleryMode) : MediaGalleryEvents
+ data class LoadMore(val direction: Timeline.PaginationDirection) : MediaGalleryEvents
+ data class Share(val eventId: EventId?) : MediaGalleryEvents
+ data class SaveOnDisk(val eventId: EventId?) : MediaGalleryEvents
+ data class OpenInfo(val mediaItem: MediaItem.Event) : MediaGalleryEvents
+ data class ViewInTimeline(val eventId: EventId) : MediaGalleryEvents
+
+ data class ConfirmDelete(
+ val eventId: EventId,
+ val mediaInfo: MediaInfo,
+ val thumbnailSource: MediaSource?,
+ ) : MediaGalleryEvents
+
+ data object CloseBottomSheet : MediaGalleryEvents
+ data class Delete(val eventId: EventId) : MediaGalleryEvents
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNavigator.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNavigator.kt
new file mode 100644
index 00000000000..7ae729309a5
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNavigator.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.matrix.api.core.EventId
+
+interface MediaGalleryNavigator {
+ fun onViewInTimelineClick(eventId: EventId)
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt
new file mode 100644
index 00000000000..0c4e3cfebc3
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
+import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactories
+
+@ContributesNode(RoomScope::class)
+class MediaGalleryNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ presenterFactory: MediaGalleryPresenter.Factory,
+ private val mediaItemPresenterFactories: MediaItemPresenterFactories,
+) : Node(buildContext, plugins = plugins),
+ MediaGalleryNavigator {
+ private val presenter = presenterFactory.create(
+ navigator = this,
+ )
+
+ interface Callback : Plugin {
+ fun onBackClick()
+ fun onItemClick(item: MediaItem.Event)
+ fun onViewInTimeline(eventId: EventId)
+ }
+
+ private fun onBackClick() {
+ plugins().forEach {
+ it.onBackClick()
+ }
+ }
+
+ override fun onViewInTimelineClick(eventId: EventId) {
+ plugins().forEach {
+ it.onViewInTimeline(eventId)
+ }
+ }
+
+ private fun onItemClick(item: MediaItem.Event) {
+ plugins().forEach {
+ it.onItemClick(item)
+ }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ CompositionLocalProvider(
+ LocalMediaItemPresenterFactories provides mediaItemPresenterFactories,
+ ) {
+ val state = presenter.present()
+ MediaGalleryView(
+ state = state,
+ onBackClick = ::onBackClick,
+ onItemClick = ::onItemClick,
+ modifier = modifier,
+ )
+ }
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt
new file mode 100644
index 00000000000..adedd835993
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import android.content.ActivityNotFoundException
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.element.android.libraries.androidutils.R
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
+import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
+import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
+import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
+import io.element.android.libraries.mediaviewer.api.local.LocalMedia
+import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
+import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+class MediaGalleryPresenter @AssistedInject constructor(
+ @Assisted private val navigator: MediaGalleryNavigator,
+ private val room: MatrixRoom,
+ private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
+ private val localMediaFactory: LocalMediaFactory,
+ private val mediaLoader: MatrixMediaLoader,
+ private val localMediaActions: LocalMediaActions,
+ private val snackbarDispatcher: SnackbarDispatcher,
+ private val mediaItemsPostProcessor: MediaItemsPostProcessor,
+) : Presenter {
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ navigator: MediaGalleryNavigator,
+ ): MediaGalleryPresenter
+ }
+
+ @Composable
+ override fun present(): MediaGalleryState {
+ val coroutineScope = rememberCoroutineScope()
+ var mode by remember { mutableStateOf(MediaGalleryMode.Images) }
+
+ val roomInfo by room.roomInfoFlow.collectAsState(null)
+
+ var mediaBottomSheetState by remember { mutableStateOf(MediaBottomSheetState.Hidden) }
+
+ var mediaItems by remember {
+ mutableStateOf>>(AsyncData.Uninitialized)
+ }
+ val groupedMediaItems by remember {
+ derivedStateOf {
+ mediaItemsPostProcessor.process(
+ mediaItems = mediaItems,
+ )
+ }
+ }
+ val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
+ localMediaActions.Configure()
+
+ var timeline by remember { mutableStateOf>(AsyncData.Uninitialized) }
+ LaunchedEffect(Unit) {
+ room.mediaTimeline()
+ .fold(
+ { timeline = AsyncData.Success(it) },
+ { timeline = AsyncData.Failure(it) },
+ )
+ }
+ DisposableEffect(Unit) {
+ onDispose {
+ timeline.dataOrNull()?.close()
+ }
+ }
+
+ MediaListEffect(
+ timeline = timeline,
+ onItemsChange = { newItems ->
+ mediaItems = newItems
+ }
+ )
+
+ fun handleEvents(event: MediaGalleryEvents) {
+ when (event) {
+ is MediaGalleryEvents.ChangeMode -> {
+ mode = event.mode
+ }
+ is MediaGalleryEvents.LoadMore -> coroutineScope.launch {
+ timeline.dataOrNull()?.paginate(event.direction)
+ }
+ is MediaGalleryEvents.Delete -> coroutineScope.delete(timeline, event.eventId)
+ is MediaGalleryEvents.SaveOnDisk -> coroutineScope.launch {
+ mediaItems.dataOrNull().find(event.eventId)?.let {
+ saveOnDisk(it)
+ }
+ }
+ is MediaGalleryEvents.Share -> coroutineScope.launch {
+ mediaItems.dataOrNull().find(event.eventId)?.let {
+ share(it)
+ }
+ }
+ is MediaGalleryEvents.ViewInTimeline -> {
+ mediaBottomSheetState = MediaBottomSheetState.Hidden
+ navigator.onViewInTimelineClick(event.eventId)
+ }
+ is MediaGalleryEvents.OpenInfo -> coroutineScope.launch {
+ mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState(
+ eventId = event.mediaItem.eventId(),
+ canDelete = when (event.mediaItem.mediaInfo().senderId) {
+ null -> false
+ room.sessionId -> room.canRedactOwn().getOrElse { false } && event.mediaItem.eventId() != null
+ else -> room.canRedactOther().getOrElse { false } && event.mediaItem.eventId() != null
+ },
+ mediaInfo = event.mediaItem.mediaInfo(),
+ thumbnailSource = when (event.mediaItem) {
+ is MediaItem.Image -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
+ is MediaItem.Video -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
+ is MediaItem.Audio -> null
+ is MediaItem.File -> null
+ is MediaItem.Voice -> null
+ },
+ )
+ }
+ is MediaGalleryEvents.ConfirmDelete -> {
+ mediaBottomSheetState = MediaBottomSheetState.MediaDeleteConfirmationState(
+ eventId = event.eventId,
+ mediaInfo = event.mediaInfo,
+ thumbnailSource = event.thumbnailSource,
+ )
+ }
+ MediaGalleryEvents.CloseBottomSheet -> {
+ mediaBottomSheetState = MediaBottomSheetState.Hidden
+ }
+ }
+ }
+
+ return MediaGalleryState(
+ roomName = roomInfo?.name ?: room.displayName,
+ mode = mode,
+ groupedMediaItems = groupedMediaItems,
+ mediaBottomSheetState = mediaBottomSheetState,
+ snackbarMessage = snackbarMessage,
+ eventSink = ::handleEvents
+ )
+ }
+
+ @Composable
+ private fun MediaListEffect(
+ timeline: AsyncData,
+ onItemsChange: (AsyncData>) -> Unit,
+ ) {
+ val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
+
+ LaunchedEffect(timeline) {
+ when (timeline) {
+ AsyncData.Uninitialized -> flowOf(AsyncData.Uninitialized)
+ is AsyncData.Failure -> flowOf(AsyncData.Failure(timeline.error))
+ is AsyncData.Loading -> flowOf(AsyncData.Loading())
+ is AsyncData.Success -> {
+ timeline.data.timelineItems
+ .onEach { items ->
+ timelineMediaItemsFactory.replaceWith(
+ timelineItems = items,
+ )
+ }
+ .launchIn(this)
+
+ timelineMediaItemsFactory.timelineItems.map { timelineItems ->
+ AsyncData.Success(timelineItems)
+ }
+ }
+ }
+ .onEach { items ->
+ updatedOnItemsChange(items)
+ }
+ .launchIn(this)
+ }
+ }
+
+ private fun CoroutineScope.delete(
+ timeline: AsyncData,
+ eventId: EventId,
+ ) = launch {
+ timeline.dataOrNull()?.redactEvent(
+ eventOrTransactionId = eventId.toEventOrTransactionId(),
+ reason = null,
+ )
+ }
+
+ private suspend fun downloadMedia(mediaItem: MediaItem.Event): Result {
+ return mediaLoader.downloadMediaFile(
+ source = mediaItem.mediaSource(),
+ mimeType = mediaItem.mediaInfo().mimeType,
+ filename = mediaItem.mediaInfo().filename
+ )
+ .mapCatching { mediaFile ->
+ localMediaFactory.createFromMediaFile(
+ mediaFile = mediaFile,
+ mediaInfo = mediaItem.mediaInfo()
+ )
+ }
+ }
+
+ private suspend fun saveOnDisk(mediaItem: MediaItem.Event) {
+ downloadMedia(mediaItem)
+ .mapCatching { localMedia ->
+ localMediaActions.saveOnDisk(localMedia)
+ }
+ .onSuccess {
+ val snackbarMessage = SnackbarMessage(CommonStrings.common_file_saved_on_disk_android)
+ snackbarDispatcher.post(snackbarMessage)
+ }
+ .onFailure {
+ val snackbarMessage = SnackbarMessage(mediaActionsError(it))
+ snackbarDispatcher.post(snackbarMessage)
+ }
+ }
+
+ private suspend fun share(mediaItem: MediaItem.Event) {
+ downloadMedia(mediaItem)
+ .mapCatching { localMedia ->
+ localMediaActions.share(localMedia)
+ }
+ .onFailure {
+ val snackbarMessage = SnackbarMessage(mediaActionsError(it))
+ snackbarDispatcher.post(snackbarMessage)
+ }
+ }
+
+ private fun mediaActionsError(throwable: Throwable): Int {
+ return if (throwable is ActivityNotFoundException) {
+ R.string.error_no_compatible_app_found
+ } else {
+ CommonStrings.error_unknown
+ }
+ }
+}
+
+private fun List?.find(eventId: EventId?): MediaItem.Event? {
+ if (this == null || eventId == null) {
+ return null
+ }
+ return filterIsInstance()
+ .firstOrNull { it.eventId() == eventId }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt
new file mode 100644
index 00000000000..51ae7941752
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryState.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
+import io.element.android.libraries.mediaviewer.impl.R
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
+import kotlinx.collections.immutable.ImmutableList
+
+data class MediaGalleryState(
+ val roomName: String,
+ val mode: MediaGalleryMode,
+ val groupedMediaItems: AsyncData,
+ val mediaBottomSheetState: MediaBottomSheetState,
+ val snackbarMessage: SnackbarMessage?,
+ val eventSink: (MediaGalleryEvents) -> Unit,
+)
+
+data class GroupedMediaItems(
+ val imageAndVideoItems: ImmutableList,
+ val fileItems: ImmutableList,
+)
+
+enum class MediaGalleryMode(val stringResource: Int) {
+ Images(R.string.screen_media_browser_list_mode_media),
+ Files(R.string.screen_media_browser_list_mode_files),
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt
new file mode 100644
index 00000000000..8876917bf60
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.components.media.aWaveForm
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
+import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemAudio
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice
+import kotlinx.collections.immutable.toImmutableList
+
+open class MediaGalleryStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aMediaGalleryState(
+ roomName = "A long room name that will be truncated",
+ ),
+ aMediaGalleryState(groupedMediaItems = AsyncData.Loading()),
+ aMediaGalleryState(groupedMediaItems = AsyncData.Success(aGroupedMediaItems())),
+ aMediaGalleryState(
+ groupedMediaItems = AsyncData.Success(
+ aGroupedMediaItems(
+ imageAndVideoItems = listOf(
+ aMediaItemDateSeparator(id = UniqueId("0")),
+ aMediaItemImage(id = UniqueId("1")),
+ aMediaItemDateSeparator(
+ id = UniqueId("2"),
+ formattedDate = "September 2004",
+ ),
+ aMediaItemImage(id = UniqueId("3")),
+ aMediaItemVideo(id = UniqueId("4")),
+ aMediaItemImage(id = UniqueId("5")),
+ aMediaItemImage(id = UniqueId("6")),
+ aMediaItemImage(id = UniqueId("7")),
+ aMediaItemImage(id = UniqueId("8")),
+ aMediaItemImage(id = UniqueId("9")),
+ aMediaItemLoadingIndicator(),
+ ).toImmutableList()
+ )
+ ),
+ ),
+ aMediaGalleryState(mode = MediaGalleryMode.Files),
+ aMediaGalleryState(mode = MediaGalleryMode.Files, groupedMediaItems = AsyncData.Loading()),
+ aMediaGalleryState(mode = MediaGalleryMode.Files, groupedMediaItems = AsyncData.Success(aGroupedMediaItems())),
+ aMediaGalleryState(
+ mode = MediaGalleryMode.Files,
+ groupedMediaItems = AsyncData.Success(
+ aGroupedMediaItems(
+ fileItems = listOf(
+ aMediaItemDateSeparator(id = UniqueId("0")),
+ aMediaItemFile(id = UniqueId("1")),
+ aMediaItemDateSeparator(
+ id = UniqueId("2"),
+ formattedDate = "September 2004",
+ ),
+ aMediaItemAudio(id = UniqueId("4")),
+ aMediaItemVoice(
+ id = UniqueId("5"),
+ waveform = aWaveForm(),
+ ),
+ aMediaItemLoadingIndicator(),
+ ).toImmutableList()
+ )
+ ),
+ ),
+ aMediaGalleryState(mediaBottomSheetState = aMediaDetailsBottomSheetState()),
+ aMediaGalleryState(
+ groupedMediaItems = AsyncData.Failure(Exception("Failed to load media")),
+ ),
+ aMediaGalleryState(
+ mode = MediaGalleryMode.Files,
+ groupedMediaItems = AsyncData.Failure(Exception("Failed to load media")),
+ ),
+ )
+}
+
+private fun aMediaGalleryState(
+ roomName: String = "Room name",
+ mode: MediaGalleryMode = MediaGalleryMode.Images,
+ groupedMediaItems: AsyncData = AsyncData.Uninitialized,
+ mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
+) = MediaGalleryState(
+ roomName = roomName,
+ mode = mode,
+ groupedMediaItems = groupedMediaItems,
+ mediaBottomSheetState = mediaBottomSheetState,
+ snackbarMessage = null,
+ eventSink = {}
+)
+
+private fun aGroupedMediaItems(
+ imageAndVideoItems: List = emptyList(),
+ fileItems: List = emptyList(),
+) = GroupedMediaItems(
+ imageAndVideoItems = imageAndVideoItems.toImmutableList(),
+ fileItems = fileItems.toImmutableList(),
+)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt
new file mode 100644
index 00000000000..6053695029e
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt
@@ -0,0 +1,497 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.designsystem.background.OnboardingBackground
+import io.element.android.libraries.designsystem.components.BigIcon
+import io.element.android.libraries.designsystem.components.PageTitle
+import io.element.android.libraries.designsystem.components.async.AsyncFailure
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.aliasScreenTitle
+import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.SegmentedButton
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
+import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.mediaviewer.impl.R
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
+import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet
+import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet
+import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
+import io.element.android.libraries.mediaviewer.impl.gallery.di.aFakeMediaItemPresenterFactories
+import io.element.android.libraries.mediaviewer.impl.gallery.di.rememberPresenter
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.AudioItemView
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.DateItemView
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.FileItemView
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.ImageItemView
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.VideoItemView
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.VoiceItemView
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
+import kotlinx.collections.immutable.ImmutableList
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MediaGalleryView(
+ state: MediaGalleryState,
+ onBackClick: () -> Unit,
+ onItemClick: (MediaItem.Event) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
+ BackHandler { onBackClick() }
+ Scaffold(
+ modifier = modifier,
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = state.roomName,
+ style = ElementTheme.typography.aliasScreenTitle,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ navigationIcon = {
+ BackButton(
+ onClick = onBackClick,
+ )
+ },
+ )
+ },
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .padding(paddingValues)
+ .consumeWindowInsets(paddingValues)
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+ SingleChoiceSegmentedButtonRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
+ MediaGalleryMode.entries.forEach { mode ->
+ SegmentedButton(
+ index = mode.ordinal,
+ count = MediaGalleryMode.entries.size,
+ selected = state.mode == mode,
+ onClick = { state.eventSink(MediaGalleryEvents.ChangeMode(mode)) },
+ text = stringResource(mode.stringResource),
+ )
+ }
+ }
+ val pagerState = rememberPagerState(0, 0f) {
+ MediaGalleryMode.entries.size
+ }
+ LaunchedEffect(state.mode) {
+ pagerState.scrollToPage(state.mode.ordinal)
+ }
+ HorizontalPager(
+ state = pagerState,
+ userScrollEnabled = false,
+ modifier = Modifier,
+ ) { page ->
+ val mode = MediaGalleryMode.entries[page]
+ MediaGalleryPage(
+ mode = mode,
+ state = state,
+ onItemClick = onItemClick,
+ )
+ }
+ }
+ }
+ when (val bottomSheetState = state.mediaBottomSheetState) {
+ MediaBottomSheetState.Hidden -> Unit
+ is MediaBottomSheetState.MediaDetailsBottomSheetState -> {
+ MediaDetailsBottomSheet(
+ state = bottomSheetState,
+ onViewInTimeline = { eventId ->
+ state.eventSink(MediaGalleryEvents.ViewInTimeline(eventId))
+ },
+ onShare = { eventId ->
+ state.eventSink(MediaGalleryEvents.Share(eventId))
+ },
+ onDownload = { eventId ->
+ state.eventSink(MediaGalleryEvents.SaveOnDisk(eventId))
+ },
+ onDelete = { eventId ->
+ state.eventSink(
+ MediaGalleryEvents.ConfirmDelete(
+ eventId = eventId,
+ mediaInfo = bottomSheetState.mediaInfo,
+ thumbnailSource = bottomSheetState.thumbnailSource,
+ )
+ )
+ },
+ onDismiss = {
+ state.eventSink(MediaGalleryEvents.CloseBottomSheet)
+ },
+ )
+ }
+ is MediaBottomSheetState.MediaDeleteConfirmationState -> {
+ MediaDeleteConfirmationBottomSheet(
+ state = bottomSheetState,
+ onDelete = {
+ state.eventSink(MediaGalleryEvents.Delete(it))
+ },
+ onDismiss = {
+ state.eventSink(MediaGalleryEvents.CloseBottomSheet)
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun MediaGalleryPage(
+ mode: MediaGalleryMode,
+ state: MediaGalleryState,
+ onItemClick: (MediaItem.Event) -> Unit,
+) {
+ when (val groupedMediaItems = state.groupedMediaItems) {
+ AsyncData.Uninitialized,
+ is AsyncData.Loading -> {
+ LoadingContent(mode)
+ }
+ is AsyncData.Success -> {
+ when (mode) {
+ MediaGalleryMode.Images -> MediaGalleryImages(
+ imagesAndVideos = groupedMediaItems.data.imageAndVideoItems,
+ eventSink = state.eventSink,
+ onItemClick = onItemClick,
+ )
+ MediaGalleryMode.Files -> MediaGalleryFiles(
+ files = groupedMediaItems.data.fileItems,
+ eventSink = state.eventSink,
+ onItemClick = onItemClick,
+ )
+ }
+ }
+ is AsyncData.Failure -> {
+ ErrorContent(
+ error = groupedMediaItems.error,
+ )
+ }
+ }
+}
+
+@Composable
+private fun MediaGalleryImages(
+ imagesAndVideos: ImmutableList,
+ eventSink: (MediaGalleryEvents) -> Unit,
+ onItemClick: (MediaItem.Event) -> Unit,
+) {
+ if (imagesAndVideos.isEmpty()) {
+ EmptyContent(
+ titleRes = R.string.screen_media_browser_media_empty_state_title,
+ subtitleRes = R.string.screen_media_browser_media_empty_state_subtitle,
+ icon = CompoundIcons.Image(),
+ )
+ } else {
+ MediaGalleryImageGrid(
+ imagesAndVideos = imagesAndVideos,
+ eventSink = eventSink,
+ onItemClick = onItemClick,
+ )
+ }
+}
+
+@Composable
+private fun MediaGalleryFiles(
+ files: ImmutableList,
+ eventSink: (MediaGalleryEvents) -> Unit,
+ onItemClick: (MediaItem.Event) -> Unit,
+) {
+ if (files.isEmpty()) {
+ EmptyContent(
+ titleRes = R.string.screen_media_browser_files_empty_state_title,
+ subtitleRes = R.string.screen_media_browser_files_empty_state_subtitle,
+ icon = CompoundIcons.Files(),
+ )
+ } else {
+ MediaGalleryFilesList(
+ files = files,
+ eventSink = eventSink,
+ onItemClick = onItemClick,
+ )
+ }
+}
+
+@Composable
+private fun MediaGalleryFilesList(
+ files: ImmutableList,
+ eventSink: (MediaGalleryEvents) -> Unit,
+ onItemClick: (MediaItem.Event) -> Unit,
+) {
+ val presenterFactories = LocalMediaItemPresenterFactories.current
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ items(
+ items = files,
+ key = { it.id() },
+ contentType = { it::class.java },
+ ) { item ->
+ when (item) {
+ is MediaItem.File -> FileItemView(
+ modifier = Modifier.animateItem(),
+ file = item,
+ onClick = { onItemClick(item) },
+ onLongClick = {
+ eventSink(MediaGalleryEvents.OpenInfo(item))
+ },
+ )
+ is MediaItem.Audio -> AudioItemView(
+ modifier = Modifier.animateItem(),
+ audio = item,
+ onClick = { onItemClick(item) },
+ onLongClick = {
+ eventSink(MediaGalleryEvents.OpenInfo(item))
+ },
+ )
+ is MediaItem.Voice -> {
+ val presenter: Presenter = presenterFactories.rememberPresenter(item)
+ VoiceItemView(
+ modifier = Modifier.animateItem(),
+ state = presenter.present(),
+ voice = item,
+ onLongClick = {
+ eventSink(MediaGalleryEvents.OpenInfo(item))
+ },
+ )
+ }
+ is MediaItem.DateSeparator -> DateItemView(
+ modifier = Modifier.animateItem(),
+ item = item
+ )
+ is MediaItem.Image,
+ is MediaItem.Video -> {
+ // Should not happen
+ }
+ is MediaItem.LoadingIndicator -> LoadingMoreIndicator(
+ modifier = Modifier.animateItem(),
+ item = item,
+ eventSink = eventSink,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun MediaGalleryImageGrid(
+ imagesAndVideos: ImmutableList,
+ eventSink: (MediaGalleryEvents) -> Unit,
+ onItemClick: (MediaItem.Event) -> Unit,
+) {
+ LazyVerticalGrid(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ columns = GridCells.Adaptive(80.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ items(
+ items = imagesAndVideos,
+ span = { item ->
+ when (item) {
+ is MediaItem.LoadingIndicator,
+ is MediaItem.DateSeparator -> GridItemSpan(maxLineSpan)
+ is MediaItem.Event -> GridItemSpan(1)
+ }
+ },
+ key = { it.id() },
+ contentType = { it::class.java },
+ ) { item ->
+ when (item) {
+ is MediaItem.DateSeparator -> DateItemView(
+ modifier = Modifier.animateItem(),
+ item = item,
+ )
+ is MediaItem.Audio -> {
+ // Should not happen
+ }
+ is MediaItem.Voice -> {
+ // Should not happen
+ }
+ is MediaItem.File -> {
+ // Should not happen
+ }
+ is MediaItem.Image -> ImageItemView(
+ modifier = Modifier.animateItem(),
+ image = item,
+ onClick = { onItemClick(item) },
+ onLongClick = {
+ eventSink(MediaGalleryEvents.OpenInfo(item))
+ },
+ )
+ is MediaItem.Video -> VideoItemView(
+ modifier = Modifier.animateItem(),
+ video = item,
+ onClick = { onItemClick(item) },
+ onLongClick = {
+ eventSink(MediaGalleryEvents.OpenInfo(item))
+ },
+ )
+ is MediaItem.LoadingIndicator -> LoadingMoreIndicator(
+ modifier = Modifier.animateItem(),
+ item = item,
+ eventSink = eventSink,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun LoadingMoreIndicator(
+ item: MediaItem.LoadingIndicator,
+ eventSink: (MediaGalleryEvents) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier.fillMaxWidth(),
+ contentAlignment = Alignment.Center,
+ ) {
+ when (item.direction) {
+ Timeline.PaginationDirection.FORWARDS -> {
+ LinearProgressIndicator(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 2.dp)
+ .height(1.dp)
+ )
+ }
+ Timeline.PaginationDirection.BACKWARDS -> {
+ CircularProgressIndicator(
+ strokeWidth = 2.dp,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ }
+ }
+ val latestEventSink by rememberUpdatedState(eventSink)
+ LaunchedEffect(item.timestamp) {
+ latestEventSink(MediaGalleryEvents.LoadMore(item.direction))
+ }
+ }
+}
+
+@Composable
+private fun ErrorContent(error: Throwable) {
+ AsyncFailure(
+ throwable = error,
+ onRetry = null,
+ modifier = Modifier.fillMaxSize(),
+ )
+}
+
+@Composable
+private fun EmptyContent(
+ titleRes: Int,
+ subtitleRes: Int,
+ icon: ImageVector,
+) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ OnboardingBackground()
+ PageTitle(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 44.dp)
+ .padding(24.dp),
+ title = stringResource(titleRes),
+ iconStyle = BigIcon.Style.Default(icon),
+ subtitle = stringResource(subtitleRes),
+ )
+ }
+}
+
+@Composable
+private fun LoadingContent(
+ mode: MediaGalleryMode,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 48.dp)
+ .padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ CircularProgressIndicator()
+ val res = when (mode) {
+ MediaGalleryMode.Images -> R.string.screen_media_browser_list_loading_media
+ MediaGalleryMode.Files -> R.string.screen_media_browser_list_loading_files
+ }
+ Text(
+ text = stringResource(res),
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ )
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun MediaGalleryViewPreview(
+ @PreviewParameter(MediaGalleryStateProvider::class) state: MediaGalleryState
+) = ElementPreview {
+ CompositionLocalProvider(
+ LocalMediaItemPresenterFactories provides aFakeMediaItemPresenterFactories(),
+ ) {
+ MediaGalleryView(
+ state = state,
+ onBackClick = {},
+ onItemClick = {},
+ )
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt
new file mode 100644
index 00000000000..05b6937c15f
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.matrix.ui.media.MediaRequestData
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+import kotlinx.collections.immutable.ImmutableList
+
+sealed interface MediaItem {
+ data class DateSeparator(
+ val id: UniqueId,
+ val formattedDate: String,
+ ) : MediaItem
+
+ data class LoadingIndicator(
+ val id: UniqueId,
+ val direction: Timeline.PaginationDirection,
+ val timestamp: Long,
+ ) : MediaItem
+
+ sealed interface Event : MediaItem
+
+ data class Image(
+ val id: UniqueId,
+ val eventId: EventId?,
+ val mediaInfo: MediaInfo,
+ val mediaSource: MediaSource,
+ val thumbnailSource: MediaSource?,
+ ) : Event {
+ val thumbnailMediaRequestData: MediaRequestData
+ get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100))
+ }
+
+ data class Video(
+ val id: UniqueId,
+ val eventId: EventId?,
+ val mediaInfo: MediaInfo,
+ val mediaSource: MediaSource,
+ val thumbnailSource: MediaSource?,
+ val duration: String?,
+ ) : Event {
+ val thumbnailMediaRequestData: MediaRequestData
+ get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100))
+ }
+
+ data class Audio(
+ val id: UniqueId,
+ val eventId: EventId?,
+ val mediaInfo: MediaInfo,
+ val mediaSource: MediaSource,
+ ) : Event
+
+ data class Voice(
+ val id: UniqueId,
+ val eventId: EventId?,
+ val mediaInfo: MediaInfo,
+ val mediaSource: MediaSource,
+ val duration: String?,
+ val waveform: ImmutableList,
+ ) : Event
+
+ data class File(
+ val id: UniqueId,
+ val eventId: EventId?,
+ val mediaInfo: MediaInfo,
+ val mediaSource: MediaSource,
+ ) : Event
+}
+
+fun MediaItem.id(): UniqueId {
+ return when (this) {
+ is MediaItem.DateSeparator -> id
+ is MediaItem.LoadingIndicator -> id
+ is MediaItem.Image -> id
+ is MediaItem.Video -> id
+ is MediaItem.File -> id
+ is MediaItem.Audio -> id
+ is MediaItem.Voice -> id
+ }
+}
+
+fun MediaItem.Event.eventId(): EventId? {
+ return when (this) {
+ is MediaItem.Image -> eventId
+ is MediaItem.Video -> eventId
+ is MediaItem.File -> eventId
+ is MediaItem.Audio -> eventId
+ is MediaItem.Voice -> eventId
+ }
+}
+
+fun MediaItem.Event.mediaInfo(): MediaInfo {
+ return when (this) {
+ is MediaItem.Image -> mediaInfo
+ is MediaItem.Video -> mediaInfo
+ is MediaItem.File -> mediaInfo
+ is MediaItem.Audio -> mediaInfo
+ is MediaItem.Voice -> mediaInfo
+ }
+}
+
+fun MediaItem.Event.mediaSource(): MediaSource {
+ return when (this) {
+ is MediaItem.Image -> mediaSource
+ is MediaItem.Video -> mediaSource
+ is MediaItem.File -> mediaSource
+ is MediaItem.Audio -> mediaSource
+ is MediaItem.Voice -> mediaSource
+ }
+}
+
+fun MediaItem.Event.thumbnailSource(): MediaSource? {
+ return when (this) {
+ is MediaItem.Image -> thumbnailSource
+ is MediaItem.Video -> thumbnailSource
+ is MediaItem.File -> null
+ is MediaItem.Audio -> null
+ is MediaItem.Voice -> null
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt
new file mode 100644
index 00000000000..dfaa39ae10f
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.architecture.AsyncData
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import javax.inject.Inject
+
+class MediaItemsPostProcessor @Inject constructor() {
+ fun process(
+ mediaItems: AsyncData>,
+ ): AsyncData {
+ return when (mediaItems) {
+ is AsyncData.Uninitialized -> AsyncData.Uninitialized
+ is AsyncData.Loading -> AsyncData.Loading()
+ is AsyncData.Failure -> AsyncData.Failure(mediaItems.error)
+ is AsyncData.Success -> AsyncData.Success(
+ mediaItems.data.process()
+ )
+ }
+ }
+
+ private fun List.process(): GroupedMediaItems {
+ val imageAndVideoItems = mutableListOf()
+ val fileItems = mutableListOf()
+
+ val imageAndVideoItemsSubList = mutableListOf()
+ val fileItemsSublist = mutableListOf()
+ forEach { item ->
+ when (item) {
+ is MediaItem.DateSeparator -> {
+ if (imageAndVideoItemsSubList.isNotEmpty()) {
+ // Date separator first
+ imageAndVideoItems.add(item)
+ // Then events
+ imageAndVideoItems.addAll(imageAndVideoItemsSubList)
+ imageAndVideoItemsSubList.clear()
+ }
+ if (fileItemsSublist.isNotEmpty()) {
+ // Date separator first
+ fileItems.add(item)
+ // Then events
+ fileItems.addAll(fileItemsSublist)
+ fileItemsSublist.clear()
+ }
+ }
+ is MediaItem.Event -> {
+ when (item) {
+ is MediaItem.Image,
+ is MediaItem.Video -> {
+ imageAndVideoItemsSubList.add(item)
+ }
+ is MediaItem.Audio,
+ is MediaItem.Voice,
+ is MediaItem.File -> {
+ fileItemsSublist.add(item)
+ }
+ }
+ }
+ is MediaItem.LoadingIndicator -> {
+ imageAndVideoItems.add(item)
+ fileItems.add(item)
+ }
+ }
+ }
+ if (imageAndVideoItemsSubList.isNotEmpty()) {
+ // Should not happen, since the SDK is always adding a date separator
+ imageAndVideoItems.addAll(imageAndVideoItemsSubList)
+ }
+ if (fileItemsSublist.isNotEmpty()) {
+ // Should not happen, since the SDK is always adding a date separator
+ fileItems.addAll(fileItemsSublist)
+ }
+ return GroupedMediaItems(
+ imageAndVideoItems = imageAndVideoItems.toImmutableList(),
+ fileItems = fileItems.toImmutableList(),
+ )
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaItemsFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaItemsFactory.kt
new file mode 100644
index 00000000000..79fcb8fd99a
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/TimelineMediaItemsFactory.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.androidutils.diff.DefaultDiffCacheInvalidator
+import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
+import io.element.android.libraries.androidutils.diff.MutableListDiffCache
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class TimelineMediaItemsFactory @Inject constructor(
+ private val dispatchers: CoroutineDispatchers,
+ private val virtualItemFactory: VirtualItemFactory,
+ private val eventItemFactory: EventItemFactory,
+) {
+ private val _timelineItems = MutableSharedFlow>(replay = 1)
+ private val lock = Mutex()
+ private val diffCache = MutableListDiffCache()
+ private val diffCacheUpdater = DiffCacheUpdater(
+ diffCache = diffCache,
+ detectMoves = false,
+ cacheInvalidator = DefaultDiffCacheInvalidator()
+ ) { old, new ->
+ if (old is MatrixTimelineItem.Event && new is MatrixTimelineItem.Event) {
+ old.uniqueId == new.uniqueId
+ } else {
+ false
+ }
+ }
+
+ val timelineItems: Flow> = _timelineItems.distinctUntilChanged()
+
+ suspend fun replaceWith(
+ timelineItems: List,
+ ) = withContext(dispatchers.computation) {
+ lock.withLock {
+ diffCacheUpdater.updateWith(timelineItems)
+ buildAndEmitTimelineItemStates(timelineItems)
+ }
+ }
+
+ private suspend fun buildAndEmitTimelineItemStates(
+ timelineItems: List,
+ ) {
+ val newTimelineItemStates = ArrayList()
+ for (index in diffCache.indices().reversed()) {
+ val cacheItem = diffCache.get(index)
+ if (cacheItem == null) {
+ buildAndCacheItem(timelineItems, index)?.also { timelineItemState ->
+ newTimelineItemStates.add(timelineItemState)
+ }
+ } else {
+ newTimelineItemStates.add(cacheItem)
+ }
+ }
+ _timelineItems.emit(newTimelineItemStates.toPersistentList())
+ }
+
+ private fun buildAndCacheItem(
+ timelineItems: List,
+ index: Int,
+ ): MediaItem? {
+ val timelineItem =
+ when (val currentTimelineItem = timelineItems[index]) {
+ is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem)
+ is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem)
+ MatrixTimelineItem.Other -> null
+ }
+ diffCache[index] = timelineItem
+ return timelineItem
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt
new file mode 100644
index 00000000000..df0976b4688
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
+import javax.inject.Inject
+
+class VirtualItemFactory @Inject constructor(
+ private val dateFormatter: DateFormatter,
+) {
+ fun create(timelineItem: MatrixTimelineItem.Virtual): MediaItem? {
+ return when (val virtual = timelineItem.virtual) {
+ is VirtualTimelineItem.DayDivider -> MediaItem.DateSeparator(
+ id = timelineItem.uniqueId,
+ formattedDate = dateFormatter.format(
+ timestamp = virtual.timestamp,
+ mode = DateFormatterMode.Month,
+ useRelative = true,
+ )
+ )
+ VirtualTimelineItem.LastForwardIndicator -> null
+ is VirtualTimelineItem.LoadingIndicator -> MediaItem.LoadingIndicator(
+ id = timelineItem.uniqueId,
+ direction = virtual.direction,
+ timestamp = virtual.timestamp
+ )
+ VirtualTimelineItem.ReadMarker -> null
+ VirtualTimelineItem.RoomBeginning -> null
+ VirtualTimelineItem.TypingNotification -> null
+ }
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt
new file mode 100644
index 00000000000..bf453c33e08
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.di
+
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
+import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
+
+/**
+ * A fake [MediaItemPresenterFactories] for screenshot tests.
+ */
+fun aFakeMediaItemPresenterFactories() = MediaItemPresenterFactories(
+ mapOf(
+ Pair(
+ MediaItem.Voice::class.java,
+ MediaItemPresenterFactory { Presenter { aVoiceMessageState() } },
+ ),
+ )
+)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/LocalMediaItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/LocalMediaItemPresenterFactories.kt
new file mode 100644
index 00000000000..8138d4c7f78
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/LocalMediaItemPresenterFactories.kt
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.di
+
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/**
+ * Provides a [MediaItemPresenterFactories] to the composition.
+ */
+val LocalMediaItemPresenterFactories = staticCompositionLocalOf {
+ MediaItemPresenterFactories(emptyMap())
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt
new file mode 100644
index 00000000000..7db70901d31
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.di
+
+import dagger.MapKey
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+import kotlin.reflect.KClass
+
+/**
+ * Annotation to add a factory of type [MediaItemPresenterFactory] to a
+ * Dagger map multi binding keyed with a subclass of [MediaItem.Event].
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@MapKey
+annotation class MediaItemEventContentKey(val value: KClass)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt
new file mode 100644
index 00000000000..28b79194c70
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.di
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import com.squareup.anvil.annotations.ContributesTo
+import dagger.Module
+import dagger.multibindings.Multibinds
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+import javax.inject.Inject
+
+/**
+ * Dagger module that declares the [MediaItemPresenterFactory] map multi binding.
+ *
+ * Its sole purpose is to support the case of an empty map multibinding.
+ */
+@Module
+@ContributesTo(RoomScope::class)
+interface MediaItemPresenterFactoriesModule {
+ @Multibinds
+ fun multiBindMediaItemPresenterFactories(): @JvmSuppressWildcards Map, MediaItemPresenterFactory<*, *>>
+}
+
+/**
+ * Room level caching layer for the [MediaItemPresenterFactory] instances.
+ *
+ * It will cache the presenter instances in the room scope, so that they can be
+ * reused across recompositions of the gallery items that happen whenever an item
+ * goes out of the [LazyColumn] viewport.
+ */
+@SingleIn(RoomScope::class)
+class MediaItemPresenterFactories @Inject constructor(
+ private val factories: @JvmSuppressWildcards Map, MediaItemPresenterFactory<*, *>>,
+) {
+ private val presenters: MutableMap> = mutableMapOf()
+
+ /**
+ * Creates and caches a presenter for the given content.
+ *
+ * Will throw if the presenter is not found in the [MediaItemPresenterFactory] map multi binding.
+ *
+ * @param C The [MediaItem.Event] subtype handled by this TimelineItem presenter.
+ * @param S The state type produced by this timeline item presenter.
+ * @param content The [MediaItem.Event] instance to create a presenter for.
+ * @param contentClass The class of [content].
+ * @return An instance of a TimelineItem presenter that will be cached in the room scope.
+ */
+ @Composable
+ fun rememberPresenter(
+ content: C,
+ contentClass: Class,
+ ): Presenter = remember(content) {
+ presenters[content]?.let {
+ @Suppress("UNCHECKED_CAST")
+ it as Presenter
+ } ?: factories.getValue(contentClass).let {
+ @Suppress("UNCHECKED_CAST")
+ (it as MediaItemPresenterFactory).create(content).apply {
+ presenters[content] = this
+ }
+ }
+ }
+}
+
+/**
+ * Creates and caches a presenter for the given content.
+ *
+ * Will throw if the presenter is not found in the [MediaItemPresenterFactory] map multi binding.
+ *
+ * @param C The [MediaItem.Event] subtype handled by this TimelineItem presenter.
+ * @param S The state type produced by this timeline item presenter.
+ * @param content The [MediaItem.Event] instance to create a presenter for.
+ * @return An instance of a TimelineItem presenter that will be cached in the room scope.
+ */
+@Composable
+inline fun MediaItemPresenterFactories.rememberPresenter(
+ content: C
+): Presenter = rememberPresenter(
+ content = content,
+ contentClass = C::class.java
+)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt
new file mode 100644
index 00000000000..fd621adbfbc
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.di
+
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+/**
+ * A factory for a [Presenter] associated with a timeline item.
+ *
+ * Implementations should be annotated with [AssistedFactory] to be created by Dagger.
+ *
+ * @param C The timeline item's [MediaItem.Event] subtype.
+ * @param S The [Presenter]'s state class.
+ * @return A [Presenter] that produces a state of type [S] for the given content of type [C].
+ */
+fun interface MediaItemPresenterFactory {
+ fun create(content: C): Presenter
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt
new file mode 100644
index 00000000000..caee363d51c
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/root/MediaGalleryRootNode.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.root
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import com.bumble.appyx.navmodel.backstack.BackStack
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.architecture.BackstackWithOverlayBox
+import io.element.android.libraries.architecture.BaseFlowNode
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.architecture.overlay.Overlay
+import io.element.android.libraries.architecture.overlay.operation.hide
+import io.element.android.libraries.architecture.overlay.operation.show
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryNode
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+import io.element.android.libraries.mediaviewer.impl.gallery.eventId
+import io.element.android.libraries.mediaviewer.impl.gallery.mediaInfo
+import io.element.android.libraries.mediaviewer.impl.gallery.mediaSource
+import io.element.android.libraries.mediaviewer.impl.gallery.thumbnailSource
+import kotlinx.parcelize.Parcelize
+
+@ContributesNode(RoomScope::class)
+class MediaGalleryRootNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ private val mediaViewerEntryPoint: MediaViewerEntryPoint
+) : BaseFlowNode(
+ backstack = BackStack(
+ initialElement = NavTarget.Root,
+ savedStateMap = buildContext.savedStateMap,
+ ),
+ overlay = Overlay(
+ savedStateMap = buildContext.savedStateMap,
+ ),
+ buildContext = buildContext,
+ plugins = plugins,
+) {
+ sealed interface NavTarget : Parcelable {
+ @Parcelize
+ data object Root : NavTarget
+
+ @Parcelize
+ data class MediaViewer(
+ val eventId: EventId?,
+ val mediaInfo: MediaInfo,
+ val mediaSource: MediaSource,
+ val thumbnailSource: MediaSource?,
+ ) : NavTarget
+ }
+
+ private fun onBackClick() {
+ plugins().forEach {
+ it.onBackClick()
+ }
+ }
+
+ private fun onViewInTimeline(eventId: EventId) {
+ plugins().forEach {
+ it.onViewInTimeline(eventId)
+ }
+ }
+
+ override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+ return when (navTarget) {
+ NavTarget.Root -> {
+ val callback = object : MediaGalleryNode.Callback {
+ override fun onBackClick() {
+ this@MediaGalleryRootNode.onBackClick()
+ }
+
+ override fun onViewInTimeline(eventId: EventId) {
+ this@MediaGalleryRootNode.onViewInTimeline(eventId)
+ }
+
+ override fun onItemClick(item: MediaItem.Event) {
+ overlay.show(
+ NavTarget.MediaViewer(
+ eventId = item.eventId(),
+ mediaInfo = item.mediaInfo(),
+ mediaSource = item.mediaSource(),
+ thumbnailSource = item.thumbnailSource(),
+ )
+ )
+ }
+ }
+ createNode(buildContext = buildContext, plugins = listOf(callback))
+ }
+ is NavTarget.MediaViewer -> {
+ val callback = object : MediaViewerEntryPoint.Callback {
+ override fun onDone() {
+ overlay.hide()
+ }
+
+ override fun onViewInTimeline(eventId: EventId) {
+ this@MediaGalleryRootNode.onViewInTimeline(eventId)
+ }
+ }
+ mediaViewerEntryPoint.nodeBuilder(this, buildContext)
+ .params(
+ MediaViewerEntryPoint.Params(
+ eventId = navTarget.eventId,
+ mediaInfo = navTarget.mediaInfo,
+ mediaSource = navTarget.mediaSource,
+ thumbnailSource = navTarget.thumbnailSource,
+ canShowInfo = true,
+ )
+ )
+ .callback(callback)
+ .build()
+ }
+ }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ BackstackWithOverlayBox(modifier)
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt
new file mode 100644
index 00000000000..d3511fcaedd
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.GraphicEq
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.core.extensions.withBrackets
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+@Composable
+fun AudioItemView(
+ audio: MediaItem.Audio,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
+ Spacer(modifier = Modifier.height(20.dp))
+ FilenameRow(
+ audio = audio,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ )
+ val caption = audio.mediaInfo.caption
+ if (caption != null) {
+ CaptionView(caption)
+ } else {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+ HorizontalDivider()
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun FilenameRow(
+ audio: MediaItem.Audio,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+) {
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(12.dp))
+ .background(
+ color = ElementTheme.colors.bgSubtleSecondary,
+ shape = RoundedCornerShape(12.dp),
+ )
+ .combinedClickable(onClick = onClick, onLongClick = onLongClick)
+ .fillMaxWidth()
+ .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier
+ .background(
+ color = ElementTheme.colors.bgActionSecondaryRest,
+ shape = CircleShape,
+ )
+ .size(32.dp)
+ .padding(6.dp),
+ imageVector = Icons.Outlined.GraphicEq,
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = audio.mediaInfo.filename,
+ modifier = Modifier.weight(1f),
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ val formattedSize = audio.mediaInfo.formattedFileSize
+ if (formattedSize.isNotEmpty()) {
+ Text(
+ text = formattedSize.withBrackets(),
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textPrimary,
+ )
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun AudioItemViewPreview(
+ @PreviewParameter(MediaItemAudioProvider::class) audio: MediaItem.Audio,
+) = ElementPreview {
+ AudioItemView(
+ audio = audio,
+ onClick = {},
+ onLongClick = {},
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/CaptionView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/CaptionView.kt
new file mode 100644
index 00000000000..6fc85336a8d
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/CaptionView.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.theme.components.Text
+
+@Composable
+fun CaptionView(
+ caption: String,
+ modifier: Modifier = Modifier,
+) {
+ Text(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ text = caption,
+ maxLines = 5,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textPrimary,
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt
new file mode 100644
index 00000000000..f0c44a382ce
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+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.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+@Composable
+fun DateItemView(
+ item: MediaItem.DateSeparator,
+ modifier: Modifier = Modifier,
+) {
+ Text(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ text = item.formattedDate,
+ textAlign = TextAlign.Center,
+ style = ElementTheme.typography.fontBodyMdMedium,
+ color = ElementTheme.colors.textPrimary,
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun DateItemViewPreview(
+ @PreviewParameter(MediaItemDateSeparatorProvider::class) date: MediaItem.DateSeparator,
+) = ElementPreview {
+ DateItemView(date)
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt
new file mode 100644
index 00000000000..5ad22349006
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.core.extensions.withBrackets
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+@Composable
+fun FileItemView(
+ file: MediaItem.File,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
+ Spacer(modifier = Modifier.height(20.dp))
+ FilenameRow(
+ file = file,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ )
+ val caption = file.mediaInfo.caption
+ if (caption != null) {
+ CaptionView(caption)
+ } else {
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+ HorizontalDivider()
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun FilenameRow(
+ file: MediaItem.File,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+) {
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(12.dp))
+ .background(
+ color = ElementTheme.colors.bgSubtleSecondary,
+ shape = RoundedCornerShape(12.dp),
+ )
+ .combinedClickable(onClick = onClick, onLongClick = onLongClick)
+ .fillMaxWidth()
+ .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier
+ .background(
+ color = ElementTheme.colors.bgActionSecondaryRest,
+ shape = CircleShape,
+ )
+ .size(32.dp)
+ .padding(6.dp),
+ imageVector = CompoundIcons.Attachment(),
+ contentDescription = null,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = file.mediaInfo.filename,
+ modifier = Modifier.weight(1f),
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ val formattedSize = file.mediaInfo.formattedFileSize
+ if (formattedSize.isNotEmpty()) {
+ Text(
+ text = formattedSize.withBrackets(),
+ style = ElementTheme.typography.fontBodyLgRegular,
+ color = ElementTheme.colors.textPrimary,
+ )
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun FileItemViewPreview(
+ @PreviewParameter(MediaItemFileProvider::class) file: MediaItem.File,
+) = ElementPreview {
+ FileItemView(
+ file = file,
+ onClick = {},
+ onLongClick = {},
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt
new file mode 100644
index 00000000000..f92a29c1ae3
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalInspectionMode
+import coil.compose.AsyncImage
+import coil.compose.AsyncImagePainter
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun ImageItemView(
+ image: MediaItem.Image,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val bgColor = if (LocalInspectionMode.current) {
+ ElementTheme.colors.bgDecorative1
+ } else {
+ Color.Transparent
+ }
+ Box(
+ modifier = modifier
+ .aspectRatio(1f)
+ .combinedClickable(onClick = onClick, onLongClick = onLongClick)
+ .background(bgColor),
+ ) {
+ var isLoaded by remember { mutableStateOf(false) }
+ AsyncImage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
+ model = image.thumbnailMediaRequestData,
+ contentScale = ContentScale.Crop,
+ alignment = Alignment.Center,
+ contentDescription = null,
+ onState = { isLoaded = it is AsyncImagePainter.State.Success },
+ )
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun ImageItemViewPreview() = ElementPreview {
+ ImageItemView(
+ image = aMediaItemImage(),
+ onClick = {},
+ onLongClick = {},
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt
new file mode 100644
index 00000000000..5d4ebe0b94b
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.core.preview.loremIpsum
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+class MediaItemAudioProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aMediaItemAudio(),
+ aMediaItemAudio(
+ filename = "A long filename that should be truncated.mp3",
+ caption = "A caption",
+ ),
+ aMediaItemAudio(
+ caption = loremIpsum,
+ ),
+ )
+}
+
+fun aMediaItemAudio(
+ id: UniqueId = UniqueId("fileId"),
+ filename: String = "filename",
+ caption: String? = null,
+): MediaItem.Audio {
+ return MediaItem.Audio(
+ id = id,
+ eventId = null,
+ mediaInfo = anAudioMediaInfo(
+ filename = filename,
+ caption = caption,
+ ),
+ mediaSource = MediaSource(""),
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt
new file mode 100644
index 00000000000..32169751f05
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+class MediaItemDateSeparatorProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aMediaItemDateSeparator(),
+ aMediaItemDateSeparator(formattedDate = "A long date that should be truncated"),
+ )
+}
+
+fun aMediaItemDateSeparator(
+ id: UniqueId = UniqueId("dateId"),
+ formattedDate: String = "October 2024",
+): MediaItem.DateSeparator {
+ return MediaItem.DateSeparator(
+ id = id,
+ formattedDate = formattedDate,
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt
new file mode 100644
index 00000000000..f5374cbbc2d
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.core.preview.loremIpsum
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+class MediaItemFileProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aMediaItemFile(),
+ aMediaItemFile(
+ filename = "A long filename that should be truncated.jpg",
+ caption = "A caption",
+ ),
+ aMediaItemFile(
+ caption = loremIpsum,
+ ),
+ )
+}
+
+fun aMediaItemFile(
+ id: UniqueId = UniqueId("fileId"),
+ filename: String = "filename",
+ caption: String? = null,
+): MediaItem.File {
+ return MediaItem.File(
+ id = id,
+ eventId = null,
+ mediaInfo = aPdfMediaInfo(
+ filename = filename,
+ caption = caption,
+ ),
+ mediaSource = MediaSource(""),
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemImageProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemImageProvider.kt
new file mode 100644
index 00000000000..a422fc715b5
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemImageProvider.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UniqueId
+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.anImageMediaInfo
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+fun aMediaItemImage(
+ id: UniqueId = UniqueId("imageId"),
+ eventId: EventId? = null,
+ senderId: UserId? = null,
+): MediaItem.Image {
+ return MediaItem.Image(
+ id = id,
+ eventId = eventId,
+ mediaInfo = anImageMediaInfo(
+ senderId = senderId,
+ ),
+ mediaSource = MediaSource(""),
+ thumbnailSource = null,
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt
new file mode 100644
index 00000000000..d9323e5979f
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemLoadingIndicatorProvider.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.api.timeline.Timeline
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+fun aMediaItemLoadingIndicator(
+ id: UniqueId = UniqueId("loadingId"),
+): MediaItem.LoadingIndicator {
+ return MediaItem.LoadingIndicator(
+ id = id,
+ direction = Timeline.PaginationDirection.BACKWARDS,
+ timestamp = 123,
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt
new file mode 100644
index 00000000000..1cc223b3479
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+class MediaItemVideoProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aMediaItemVideo(),
+ aMediaItemVideo(
+ duration = null,
+ ),
+ )
+}
+
+fun aMediaItemVideo(
+ id: UniqueId = UniqueId("videoId"),
+ mediaSource: MediaSource = MediaSource(""),
+ duration: String? = "1:23",
+): MediaItem.Video {
+ return MediaItem.Video(
+ id = id,
+ eventId = null,
+ mediaInfo = aVideoMediaInfo(),
+ mediaSource = mediaSource,
+ thumbnailSource = null,
+ duration = duration,
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt
new file mode 100644
index 00000000000..8056c094e81
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.core.preview.loremIpsum
+import io.element.android.libraries.designsystem.components.media.aWaveForm
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+import kotlinx.collections.immutable.toImmutableList
+
+class MediaItemVoiceProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aMediaItemVoice(),
+ aMediaItemVoice(
+ filename = "A long filename that should be truncated.ogg",
+ caption = "A caption",
+ ),
+ aMediaItemVoice(
+ caption = loremIpsum,
+ ),
+ aMediaItemVoice(
+ waveform = emptyList(),
+ ),
+ )
+}
+
+fun aMediaItemVoice(
+ id: UniqueId = UniqueId("fileId"),
+ filename: String = "filename.ogg",
+ caption: String? = null,
+ duration: String? = "1:23",
+ waveform: List = aWaveForm(),
+): MediaItem.Voice {
+ return MediaItem.Voice(
+ id = id,
+ eventId = null,
+ mediaInfo = aVoiceMediaInfo(
+ filename = filename,
+ caption = caption,
+ ),
+ mediaSource = MediaSource(""),
+ duration = duration,
+ waveform = waveform.toImmutableList(),
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt
new file mode 100644
index 00000000000..0837bd73b27
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import coil.compose.AsyncImagePainter
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun VideoItemView(
+ video: MediaItem.Video,
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val bgColor = if (LocalInspectionMode.current) {
+ ElementTheme.colors.bgDecorative2
+ } else {
+ Color.Transparent
+ }
+ Box(
+ modifier = modifier
+ .aspectRatio(1f)
+ .combinedClickable(onClick = onClick, onLongClick = onLongClick)
+ .background(bgColor),
+ ) {
+ var isLoaded by remember { mutableStateOf(false) }
+ AsyncImage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
+ model = video.thumbnailMediaRequestData,
+ contentScale = ContentScale.Crop,
+ alignment = Alignment.Center,
+ contentDescription = null,
+ onState = { isLoaded = it is AsyncImagePainter.State.Success },
+ )
+ VideoInfoRow(
+ video = video,
+ modifier = Modifier.align(Alignment.BottomStart)
+ )
+ }
+}
+
+@Composable
+private fun VideoInfoRow(
+ video: MediaItem.Video,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ ElementTheme.colors.bgCanvasDefault.copy(alpha = 0f),
+ ElementTheme.colors.bgCanvasDefault,
+ )
+ )
+ )
+ .padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier.size(20.dp),
+ imageVector = CompoundIcons.VideoCallSolid(),
+ contentDescription = null
+ )
+ if (video.duration != null) {
+ Spacer(Modifier.weight(1f))
+ Text(
+ text = video.duration,
+ style = ElementTheme.typography.fontBodySmMedium,
+ color = ElementTheme.colors.textPrimary,
+ )
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun VideoItemViewPreview(
+ @PreviewParameter(MediaItemVideoProvider::class) video: MediaItem.Video,
+) = ElementPreview {
+ VideoItemView(
+ video = video,
+ onClick = {},
+ onLongClick = {},
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt
new file mode 100644
index 00000000000..ab322427eef
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.ui
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.IconButton
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
+import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
+import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.delay
+
+@Composable
+fun VoiceItemView(
+ state: VoiceMessageState,
+ voice: MediaItem.Voice,
+ onLongClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
+ Spacer(modifier = Modifier.height(20.dp))
+ VoiceInfoRow(
+ state = state,
+ voice = voice,
+ onLongClick = onLongClick,
+ )
+ val caption = voice.mediaInfo.caption
+ if (caption != null) {
+ CaptionView(caption)
+ } else {
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ HorizontalDivider()
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun VoiceInfoRow(
+ state: VoiceMessageState,
+ voice: MediaItem.Voice,
+ onLongClick: () -> Unit,
+) {
+ fun playPause() {
+ state.eventSink(VoiceMessageEvents.PlayPause)
+ }
+
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(12.dp))
+ .background(
+ color = ElementTheme.colors.bgSubtleSecondary,
+ shape = RoundedCornerShape(12.dp),
+ )
+ .combinedClickable(onClick = {}, onLongClick = onLongClick)
+ .fillMaxWidth()
+ .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ when (state.button) {
+ VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause)
+ VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause)
+ VoiceMessageState.Button.Downloading -> ProgressButton()
+ VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause)
+ VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false)
+ }
+ Spacer(Modifier.width(8.dp))
+ Text(
+ text = if (state.progress > 0f) state.time else voice.duration ?: state.time,
+ color = ElementTheme.colors.textSecondary,
+ style = ElementTheme.typography.fontBodyMdMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ WaveformPlaybackView(
+ modifier = Modifier
+ .weight(1f)
+ .height(34.dp),
+ showCursor = state.showCursor,
+ playbackProgress = state.progress,
+ waveform = voice.waveform.toPersistentList(),
+ onSeek = {
+ state.eventSink(VoiceMessageEvents.Seek(it))
+ },
+ seekEnabled = true,
+ )
+ }
+}
+
+/**
+ * Progress button is shown when the voice message is being downloaded.
+ *
+ * The progress indicator is optimistic and displays a pause button (which
+ * indicates the audio is playing) for 2 seconds before revealing the
+ * actual progress indicator.
+ */
+@Composable
+private fun ProgressButton(
+ displayImmediately: Boolean = false,
+) {
+ var canDisplay by remember { mutableStateOf(displayImmediately) }
+ LaunchedEffect(Unit) {
+ delay(2000L)
+ canDisplay = true
+ }
+ CustomIconButton(
+ onClick = {},
+ enabled = false,
+ ) {
+ if (canDisplay) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(2.dp)
+ .size(16.dp),
+ color = ElementTheme.colors.iconSecondary,
+ strokeWidth = 2.dp,
+ )
+ } else {
+ ControlIcon(
+ imageVector = CompoundIcons.PauseSolid(),
+ contentDescription = stringResource(id = CommonStrings.a11y_pause),
+ )
+ }
+ }
+}
+
+@Composable
+private fun PlayButton(
+ onClick: () -> Unit,
+ enabled: Boolean = true,
+) {
+ CustomIconButton(
+ onClick = onClick,
+ enabled = enabled,
+ ) {
+ ControlIcon(
+ imageVector = CompoundIcons.PlaySolid(),
+ contentDescription = stringResource(id = CommonStrings.a11y_play),
+ )
+ }
+}
+
+@Composable
+private fun PauseButton(
+ onClick: () -> Unit,
+) {
+ CustomIconButton(
+ onClick = onClick,
+ ) {
+ ControlIcon(
+ imageVector = CompoundIcons.PauseSolid(),
+ contentDescription = stringResource(id = CommonStrings.a11y_pause),
+ )
+ }
+}
+
+@Composable
+private fun RetryButton(
+ onClick: () -> Unit,
+) {
+ CustomIconButton(
+ onClick = onClick,
+ ) {
+ ControlIcon(
+ imageVector = CompoundIcons.Restart(),
+ contentDescription = stringResource(id = CommonStrings.action_retry),
+ )
+ }
+}
+
+@Composable
+private fun ControlIcon(
+ imageVector: ImageVector,
+ contentDescription: String?,
+) {
+ Icon(
+ modifier = Modifier.padding(vertical = 10.dp),
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ )
+}
+
+@Composable
+private fun CustomIconButton(
+ onClick: () -> Unit,
+ enabled: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ IconButton(
+ onClick = onClick,
+ modifier = Modifier
+ .background(color = ElementTheme.colors.bgCanvasDefault, shape = CircleShape)
+ .border(
+ width = 1.dp,
+ color = ElementTheme.colors.borderInteractiveSecondary,
+ shape = CircleShape,
+ )
+ .size(36.dp),
+ enabled = enabled,
+ colors = IconButtonDefaults.iconButtonColors(
+ contentColor = ElementTheme.colors.iconSecondary,
+ disabledContentColor = ElementTheme.colors.iconDisabled,
+ ),
+ content = content,
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun VoiceItemViewPreview(
+ @PreviewParameter(MediaItemVoiceProvider::class) voice: MediaItem.Voice,
+) = ElementPreview {
+ VoiceItemView(
+ state = aVoiceMessageState(),
+ voice = voice,
+ onLongClick = {},
+ )
+}
+
+@PreviewsDayNight
+@Composable
+internal fun VoiceItemViewPlayPreview(
+ @PreviewParameter(VoiceMessageStateProvider::class) state: VoiceMessageState,
+) = ElementPreview {
+ VoiceItemView(
+ state = state,
+ voice = aMediaItemVoice(),
+ onLongClick = {},
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt
new file mode 100644
index 00000000000..9f5a6593eda
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery.voice
+
+import androidx.compose.runtime.Composable
+import com.squareup.anvil.annotations.ContributesTo
+import dagger.Binds
+import dagger.Module
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.multibindings.IntoMap
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
+import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemEventContentKey
+import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactory
+import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
+import kotlin.time.Duration
+
+@Module
+@ContributesTo(RoomScope::class)
+interface VoiceMessagePresenterModule {
+ @Binds
+ @IntoMap
+ @MediaItemEventContentKey(MediaItem.Voice::class)
+ fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): MediaItemPresenterFactory<*, *>
+}
+
+class VoiceMessagePresenter @AssistedInject constructor(
+ voiceMessagePresenterFactory: VoiceMessagePresenterFactory,
+ @Assisted private val item: MediaItem.Voice,
+) : Presenter {
+ @AssistedFactory
+ fun interface Factory : MediaItemPresenterFactory {
+ override fun create(content: MediaItem.Voice): VoiceMessagePresenter
+ }
+
+ private val presenter = voiceMessagePresenterFactory.createVoiceMessagePresenter(
+ eventId = item.eventId,
+ mediaSource = item.mediaSource,
+ mimeType = item.mediaInfo.mimeType,
+ filename = item.mediaInfo.filename,
+ // TODO Get the duration for the fallback?
+ duration = Duration.ZERO,
+ )
+
+ @Composable
+ override fun present(): VoiceMessageState {
+ return presenter.present()
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt
index 8b5163cf6c1..ceed35121a7 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt
@@ -18,6 +18,7 @@ import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
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.matrix.api.core.UserId
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.MediaInfo
@@ -41,8 +42,12 @@ class AndroidLocalMediaFactory @Inject constructor(
name = mediaInfo.filename,
caption = mediaInfo.caption,
formattedFileSize = mediaInfo.formattedFileSize,
+ senderId = mediaInfo.senderId,
senderName = mediaInfo.senderName,
+ senderAvatar = mediaInfo.senderAvatar,
dateSent = mediaInfo.dateSent,
+ dateSentFull = mediaInfo.dateSentFull,
+ waveform = mediaInfo.waveform,
)
override fun createFromUri(
@@ -56,8 +61,12 @@ class AndroidLocalMediaFactory @Inject constructor(
name = name,
caption = null,
formattedFileSize = formattedFileSize,
+ senderId = null,
senderName = null,
+ senderAvatar = null,
dateSent = null,
+ dateSentFull = null,
+ waveform = null,
)
private fun createFromUri(
@@ -66,8 +75,12 @@ class AndroidLocalMediaFactory @Inject constructor(
name: String?,
caption: String?,
formattedFileSize: String?,
+ senderId: UserId?,
senderName: String?,
+ senderAvatar: String?,
dateSent: String?,
+ dateSentFull: String?,
+ waveform: List?,
): LocalMedia {
val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream
val fileName = name ?: context.getFileName(uri) ?: ""
@@ -81,8 +94,12 @@ class AndroidLocalMediaFactory @Inject constructor(
caption = caption,
formattedFileSize = fileSize,
fileExtension = fileExtension,
+ senderId = senderId,
senderName = senderName,
+ senderAvatar = senderAvatar,
dateSent = dateSent,
+ dateSentFull = dateSentFull,
+ waveform = waveform,
)
)
}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt
index 5d0a2993df2..1c56c291b47 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt
@@ -10,10 +10,12 @@ package io.element.android.libraries.mediaviewer.impl.local
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.core.mimetype.MimeTypes
+import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
+import io.element.android.libraries.mediaviewer.impl.local.audio.MediaAudioView
import io.element.android.libraries.mediaviewer.impl.local.file.MediaFileView
import io.element.android.libraries.mediaviewer.impl.local.image.MediaImageView
import io.element.android.libraries.mediaviewer.impl.local.pdf.MediaPdfView
@@ -48,7 +50,13 @@ fun LocalMediaView(
modifier = modifier,
onClick = onClick,
)
- // TODO handle audio with exoplayer
+ mimeType.isMimeTypeAudio() -> MediaAudioView(
+ localMediaViewState = localMediaViewState,
+ bottomPaddingInPixels = bottomPaddingInPixels,
+ localMedia = localMedia,
+ info = mediaInfo,
+ modifier = modifier,
+ )
else -> MediaFileView(
localMediaViewState = localMediaViewState,
uri = localMedia?.uri,
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt
new file mode 100644
index 00000000000..05c96cc8afc
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt
@@ -0,0 +1,366 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.local.audio
+
+import android.annotation.SuppressLint
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.FrameLayout
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.GraphicEq
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaMetadata
+import androidx.media3.common.Player
+import androidx.media3.common.Timeline
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.AspectRatioFrameLayout
+import androidx.media3.ui.PlayerView
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.text.toDp
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
+import io.element.android.libraries.mediaviewer.api.local.LocalMedia
+import io.element.android.libraries.mediaviewer.impl.local.LocalMediaViewState
+import io.element.android.libraries.mediaviewer.impl.local.PlayableState
+import io.element.android.libraries.mediaviewer.impl.local.player.MediaPlayerControllerState
+import io.element.android.libraries.mediaviewer.impl.local.player.MediaPlayerControllerView
+import io.element.android.libraries.mediaviewer.impl.local.player.rememberExoPlayer
+import io.element.android.libraries.mediaviewer.impl.local.player.seekToEnsurePlaying
+import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay
+import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.delay
+
+@SuppressLint("UnsafeOptInUsageError")
+@Composable
+fun MediaAudioView(
+ localMediaViewState: LocalMediaViewState,
+ bottomPaddingInPixels: Int,
+ localMedia: LocalMedia?,
+ info: MediaInfo?,
+ modifier: Modifier = Modifier,
+) {
+ val exoPlayer = rememberExoPlayer()
+ ExoPlayerMediaAudioView(
+ localMediaViewState = localMediaViewState,
+ bottomPaddingInPixels = bottomPaddingInPixels,
+ exoPlayer = exoPlayer,
+ localMedia = localMedia,
+ info = info,
+ modifier = modifier,
+ )
+}
+
+@SuppressLint("UnsafeOptInUsageError")
+@Composable
+private fun ExoPlayerMediaAudioView(
+ localMediaViewState: LocalMediaViewState,
+ bottomPaddingInPixels: Int,
+ exoPlayer: ExoPlayer,
+ localMedia: LocalMedia?,
+ info: MediaInfo?,
+ modifier: Modifier = Modifier,
+) {
+ var mediaPlayerControllerState: MediaPlayerControllerState by remember {
+ mutableStateOf(
+ MediaPlayerControllerState(
+ isVisible = true,
+ isPlaying = false,
+ progressInMillis = 0,
+ durationInMillis = 0,
+ canMute = false,
+ isMuted = false,
+ )
+ )
+ }
+
+ var metadata: MediaMetadata? by remember {
+ mutableStateOf(null)
+ }
+
+ val playableState: PlayableState.Playable by remember {
+ derivedStateOf {
+ PlayableState.Playable(
+ isShowingControls = mediaPlayerControllerState.isVisible,
+ )
+ }
+ }
+
+ localMediaViewState.playableState = playableState
+
+ val playerListener = remember {
+ object : Player.Listener {
+ override fun onRenderedFirstFrame() {
+ localMediaViewState.isReady = true
+ }
+
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
+ mediaPlayerControllerState = mediaPlayerControllerState.copy(
+ isPlaying = isPlaying,
+ )
+ }
+
+ override fun onTimelineChanged(timeline: Timeline, reason: Int) {
+ if (reason == Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) {
+ exoPlayer.duration.takeIf { it >= 0 }
+ ?.let {
+ mediaPlayerControllerState = mediaPlayerControllerState.copy(
+ durationInMillis = it,
+ )
+ }
+ }
+ }
+
+ override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
+ metadata = mediaMetadata
+ }
+ }
+ }
+
+ LaunchedEffect(exoPlayer.isPlaying) {
+ if (exoPlayer.isPlaying) {
+ while (true) {
+ mediaPlayerControllerState = mediaPlayerControllerState.copy(
+ progressInMillis = exoPlayer.currentPosition,
+ )
+ delay(200)
+ }
+ } else {
+ // Ensure we render the final state
+ mediaPlayerControllerState = mediaPlayerControllerState.copy(
+ progressInMillis = exoPlayer.currentPosition,
+ )
+ }
+ }
+ if (localMedia?.uri != null) {
+ LaunchedEffect(localMedia.uri) {
+ val mediaItem = MediaItem.fromUri(localMedia.uri)
+ exoPlayer.setMediaItem(mediaItem)
+ }
+ } else {
+ exoPlayer.setMediaItems(emptyList())
+ }
+ val context = LocalContext.current
+ val waveform = info?.waveform
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .background(ElementTheme.colors.bgSubtlePrimary),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ contentAlignment = Alignment.Center,
+ ) {
+ if (LocalInspectionMode.current) {
+ Text(
+ modifier = Modifier
+ .padding(16.dp)
+ .width(240.dp),
+ text = "An audio Player may render an image here if the audio file contains some artwork.",
+ textAlign = TextAlign.Center,
+ color = ElementTheme.colors.textPrimary,
+ )
+ } else {
+ AndroidView(
+ modifier = Modifier
+ .clip(shape = RoundedCornerShape(12.dp))
+ .clipToBounds()
+ .width(240.dp),
+ factory = {
+ PlayerView(context).apply {
+ player = exoPlayer
+ resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
+ layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ useController = false
+ }
+ },
+ update = { playerView ->
+ playerView.isVisible = metadata.hasArtwork()
+ },
+ onRelease = { playerView ->
+ playerView.player = null
+ },
+ )
+ }
+ if (waveform != null) {
+ WaveformPlaybackView(
+ modifier = Modifier
+ .height(48.dp),
+ playbackProgress = mediaPlayerControllerState.progressAsFloat,
+ showCursor = true,
+ waveform = waveform.toPersistentList(),
+ onSeek = {
+ exoPlayer.seekToEnsurePlaying((it * exoPlayer.duration).toLong())
+ },
+ seekEnabled = true,
+ )
+ } else {
+ if (!metadata.hasArtwork()) {
+ Box(
+ modifier = Modifier
+ .size(72.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.onBackground),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.GraphicEq,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.background,
+ modifier = Modifier
+ .size(32.dp),
+ )
+ }
+ }
+ }
+ }
+ if (waveform == null) {
+ // Display the info below the player
+ AudioInfoView(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ info = info,
+ metadata = metadata,
+ )
+ }
+ }
+ MediaPlayerControllerView(
+ state = mediaPlayerControllerState,
+ onTogglePlay = {
+ exoPlayer.togglePlay()
+ },
+ onSeekChange = {
+ exoPlayer.seekToEnsurePlaying(it.toLong())
+ },
+ onToggleMute = {
+ // Cannot happen for audio files
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .padding(bottom = bottomPaddingInPixels.toDp()),
+ )
+ }
+
+ OnLifecycleEvent { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_CREATE -> exoPlayer.addListener(playerListener)
+ Lifecycle.Event.ON_RESUME -> exoPlayer.prepare()
+ Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
+ Lifecycle.Event.ON_DESTROY -> {
+ exoPlayer.release()
+ exoPlayer.removeListener(playerListener)
+ }
+ else -> Unit
+ }
+ }
+}
+
+@Composable
+private fun AudioInfoView(
+ info: MediaInfo?,
+ metadata: MediaMetadata?,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ // Render the info about the file and from the metadata
+ val metaDataInfo = metadata.buildInfo()
+ if (metaDataInfo.isNotEmpty()) {
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = metaDataInfo,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ if (info != null) {
+ Spacer(modifier = Modifier.height(24.dp))
+ Text(
+ text = info.filename,
+ maxLines = 2,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = formatFileExtensionAndSize(info.fileExtension, info.formattedFileSize),
+ style = ElementTheme.typography.fontBodyMdRegular,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun MediaAudioViewPreview(
+ @PreviewParameter(MediaInfoAudioProvider::class) info: MediaInfo
+) = ElementPreview {
+ MediaAudioView(
+ modifier = Modifier.fillMaxSize(),
+ bottomPaddingInPixels = 0,
+ localMediaViewState = rememberLocalMediaViewState(),
+ info = info,
+ localMedia = null,
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioProvider.kt
new file mode 100644
index 00000000000..87f9bc37355
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioProvider.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.local.audio
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.designsystem.components.media.aWaveForm
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
+
+open class MediaInfoAudioProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ anAudioMediaInfo(),
+ anAudioMediaInfo(
+ waveForm = aWaveForm(),
+ ),
+ )
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaMetadata.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaMetadata.kt
new file mode 100644
index 00000000000..d49559e1df2
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaMetadata.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.local.audio
+
+import androidx.media3.common.MediaMetadata
+
+fun MediaMetadata?.hasArtwork(): Boolean {
+ return this?.artworkData != null || this?.artworkUri != null
+}
+
+fun MediaMetadata?.buildInfo(): String {
+ this ?: return ""
+ return buildString {
+ if (artist != null) {
+ append(artist)
+ }
+ if (title != null) {
+ if (isNotEmpty()) {
+ append(" - ")
+ }
+ append(title)
+ }
+ if (recordingYear != null) {
+ if (isNotEmpty()) {
+ append(" - ")
+ }
+ append(recordingYear)
+ }
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt
index 980f9eba89a..08d906dd98b 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt
@@ -10,12 +10,10 @@ package io.element.android.libraries.mediaviewer.impl.local.file
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo
-import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
open class MediaInfoFileProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aPdfMediaInfo(),
- anAudioMediaInfo(),
)
}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerExtensions.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerExtensions.kt
new file mode 100644
index 00000000000..2de6d62065e
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerExtensions.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.local.player
+
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.ExoPlayer
+
+fun ExoPlayer.togglePlay() {
+ if (isPlaying) {
+ pause()
+ } else {
+ if (playbackState == Player.STATE_ENDED) {
+ seekTo(0)
+ } else {
+ play()
+ }
+ }
+}
+
+fun ExoPlayer.seekToEnsurePlaying(positionMs: Long) {
+ if (isPlaying.not()) {
+ play()
+ }
+ seekTo(positionMs)
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerFactory.kt
new file mode 100644
index 00000000000..0baf3d7e9dc
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerFactory.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.local.player
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.media3.exoplayer.ExoPlayer
+
+@Composable
+fun rememberExoPlayer(): ExoPlayer {
+ return if (LocalInspectionMode.current) {
+ remember {
+ ExoPlayerForPreview()
+ }
+ } else {
+ val context = LocalContext.current
+ remember {
+ ExoPlayer.Builder(context).build()
+ }
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerForPreview.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt
similarity index 99%
rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerForPreview.kt
rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt
index c5176375767..0626c78f283 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/ExoPlayerForPreview.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt
@@ -11,7 +11,7 @@
"DEPRECATION",
)
-package io.element.android.libraries.mediaviewer.impl.local.video
+package io.element.android.libraries.mediaviewer.impl.local.player
import android.annotation.SuppressLint
import android.media.AudioDeviceInfo
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerState.kt
similarity index 54%
rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerState.kt
rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerState.kt
index c4e4b913a72..41f6225ee9c 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerState.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerState.kt
@@ -5,12 +5,18 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.libraries.mediaviewer.impl.local.video
+package io.element.android.libraries.mediaviewer.impl.local.player
+
+import androidx.annotation.FloatRange
data class MediaPlayerControllerState(
val isVisible: Boolean,
val isPlaying: Boolean,
val progressInMillis: Long,
val durationInMillis: Long,
+ val canMute: Boolean,
val isMuted: Boolean,
-)
+) {
+ @FloatRange(from = 0.0, to = 1.0)
+ val progressAsFloat = (progressInMillis.toFloat() / durationInMillis.toFloat()).coerceIn(0f, 1f)
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt
similarity index 84%
rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerStateProvider.kt
rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt
index 78059bd4eb7..1528c619c33 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaPlayerControllerStateProvider.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.libraries.mediaviewer.impl.local.video
+package io.element.android.libraries.mediaviewer.impl.local.player
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@@ -18,6 +18,9 @@ open class MediaPlayerControllerStateProvider : PreviewParameterProvider= 0 }
- ?.let {
- mediaPlayerControllerState = mediaPlayerControllerState.copy(
- durationInMillis = it,
- )
- }
+ override fun onTimelineChanged(timeline: Timeline, reason: Int) {
+ if (reason == Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE) {
+ exoPlayer.duration.takeIf { it >= 0 }
+ ?.let {
+ mediaPlayerControllerState = mediaPlayerControllerState.copy(
+ durationInMillis = it,
+ )
+ }
+ }
}
}
}
@@ -211,22 +210,11 @@ private fun ExoPlayerMediaVideoView(
state = mediaPlayerControllerState,
onTogglePlay = {
autoHideController++
- if (exoPlayer.isPlaying) {
- exoPlayer.pause()
- } else {
- if (exoPlayer.playbackState == Player.STATE_ENDED) {
- exoPlayer.seekTo(0)
- } else {
- exoPlayer.play()
- }
- }
+ exoPlayer.togglePlay()
},
onSeekChange = {
autoHideController++
- if (exoPlayer.isPlaying.not()) {
- exoPlayer.play()
- }
- exoPlayer.seekTo(it.toLong())
+ exoPlayer.seekToEnsurePlaying(it.toLong())
},
onToggleMute = {
autoHideController++
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt
index ac2714584c1..6d9a31a8163 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt
@@ -7,10 +7,17 @@
package io.element.android.libraries.mediaviewer.impl.viewer
+import io.element.android.libraries.matrix.api.core.EventId
+
sealed interface MediaViewerEvents {
data object SaveOnDisk : MediaViewerEvents
data object Share : MediaViewerEvents
data object OpenWith : MediaViewerEvents
data object RetryLoading : MediaViewerEvents
data object ClearLoadingError : MediaViewerEvents
+ data class ViewInTimeline(val eventId: EventId) : MediaViewerEvents
+ data object OpenInfo : MediaViewerEvents
+ data class ConfirmDelete(val eventId: EventId) : MediaViewerEvents
+ data object CloseBottomSheet : MediaViewerEvents
+ data class Delete(val eventId: EventId) : MediaViewerEvents
}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt
new file mode 100644
index 00000000000..07fa0ec15d2
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNavigator.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.viewer
+
+import io.element.android.libraries.matrix.api.core.EventId
+
+interface MediaViewerNavigator {
+ fun onViewInTimelineClick(eventId: EventId)
+ fun onItemDeleted()
+}
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt
index 83c7c1aca76..9a9af5ee633 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt
@@ -19,14 +19,16 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
@ContributesNode(RoomScope::class)
-open class MediaViewerNode @AssistedInject constructor(
+class MediaViewerNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
presenterFactory: MediaViewerPresenter.Factory,
-) : Node(buildContext, plugins = plugins) {
+) : Node(buildContext, plugins = plugins),
+ MediaViewerNavigator {
private val inputs = inputs()
private fun onDone() {
@@ -35,7 +37,20 @@ open class MediaViewerNode @AssistedInject constructor(
}
}
- private val presenter = presenterFactory.create(inputs)
+ override fun onViewInTimelineClick(eventId: EventId) {
+ plugins().forEach {
+ it.onViewInTimeline(eventId)
+ }
+ }
+
+ override fun onItemDeleted() {
+ onDone()
+ }
+
+ private val presenter = presenterFactory.create(
+ inputs = inputs,
+ navigator = this,
+ )
@Composable
override fun View(modifier: Modifier) {
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt
index 068fb02b0f7..a9079347244 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt
@@ -25,11 +25,17 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
+import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
+import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
@@ -38,6 +44,8 @@ import io.element.android.libraries.androidutils.R as UtilsR
class MediaViewerPresenter @AssistedInject constructor(
@Assisted private val inputs: MediaViewerEntryPoint.Params,
+ @Assisted private val navigator: MediaViewerNavigator,
+ private val room: MatrixRoom,
private val localMediaFactory: LocalMediaFactory,
private val mediaLoader: MatrixMediaLoader,
private val localMediaActions: LocalMediaActions,
@@ -45,7 +53,10 @@ class MediaViewerPresenter @AssistedInject constructor(
) : Presenter {
@AssistedFactory
interface Factory {
- fun create(inputs: MediaViewerEntryPoint.Params): MediaViewerPresenter
+ fun create(
+ inputs: MediaViewerEntryPoint.Params,
+ navigator: MediaViewerNavigator,
+ ): MediaViewerPresenter
}
@Composable
@@ -66,24 +77,65 @@ class MediaViewerPresenter @AssistedInject constructor(
mediaFile.value?.close()
}
}
+ var mediaBottomSheetState by remember { mutableStateOf(MediaBottomSheetState.Hidden) }
fun handleEvents(mediaViewerEvents: MediaViewerEvents) {
when (mediaViewerEvents) {
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
MediaViewerEvents.ClearLoadingError -> localMedia.value = AsyncData.Uninitialized
- MediaViewerEvents.SaveOnDisk -> coroutineScope.saveOnDisk(localMedia.value)
- MediaViewerEvents.Share -> coroutineScope.share(localMedia.value)
- MediaViewerEvents.OpenWith -> coroutineScope.open(localMedia.value)
+ MediaViewerEvents.SaveOnDisk -> {
+ mediaBottomSheetState = MediaBottomSheetState.Hidden
+ coroutineScope.saveOnDisk(localMedia.value)
+ }
+ MediaViewerEvents.Share -> {
+ mediaBottomSheetState = MediaBottomSheetState.Hidden
+ coroutineScope.share(localMedia.value)
+ }
+ MediaViewerEvents.OpenWith -> {
+ mediaBottomSheetState = MediaBottomSheetState.Hidden
+ coroutineScope.open(localMedia.value)
+ }
+ is MediaViewerEvents.Delete -> {
+ mediaBottomSheetState = MediaBottomSheetState.Hidden
+ coroutineScope.delete(mediaViewerEvents.eventId)
+ }
+ is MediaViewerEvents.ViewInTimeline -> {
+ mediaBottomSheetState = MediaBottomSheetState.Hidden
+ navigator.onViewInTimelineClick(mediaViewerEvents.eventId)
+ }
+ MediaViewerEvents.OpenInfo -> coroutineScope.launch {
+ mediaBottomSheetState = MediaBottomSheetState.MediaDetailsBottomSheetState(
+ eventId = inputs.eventId,
+ canDelete = when (inputs.mediaInfo.senderId) {
+ null -> false
+ room.sessionId -> room.canRedactOwn().getOrElse { false } && inputs.eventId != null
+ else -> room.canRedactOther().getOrElse { false } && inputs.eventId != null
+ },
+ mediaInfo = inputs.mediaInfo,
+ thumbnailSource = inputs.thumbnailSource,
+ )
+ }
+ is MediaViewerEvents.ConfirmDelete -> {
+ mediaBottomSheetState = MediaBottomSheetState.MediaDeleteConfirmationState(
+ eventId = mediaViewerEvents.eventId,
+ mediaInfo = inputs.mediaInfo,
+ thumbnailSource = inputs.thumbnailSource ?: inputs.mediaSource,
+ )
+ }
+ MediaViewerEvents.CloseBottomSheet -> {
+ mediaBottomSheetState = MediaBottomSheetState.Hidden
+ }
}
}
return MediaViewerState(
+ eventId = inputs.eventId,
mediaInfo = inputs.mediaInfo,
thumbnailSource = inputs.thumbnailSource,
downloadedMedia = localMedia.value,
snackbarMessage = snackbarMessage,
- canDownload = inputs.canDownload,
- canShare = inputs.canShare,
+ canShowInfo = inputs.canShowInfo,
+ mediaBottomSheetState = mediaBottomSheetState,
eventSink = ::handleEvents
)
}
@@ -126,6 +178,17 @@ class MediaViewerPresenter @AssistedInject constructor(
}
}
+ private fun CoroutineScope.delete(eventId: EventId) = launch {
+ room.liveTimeline.redactEvent(eventId.toEventOrTransactionId(), null)
+ .onFailure {
+ val snackbarMessage = SnackbarMessage(CommonStrings.error_unknown)
+ snackbarDispatcher.post(snackbarMessage)
+ }
+ .onSuccess {
+ navigator.onItemDeleted()
+ }
+ }
+
private fun CoroutineScope.share(localMedia: AsyncData) = launch {
if (localMedia is AsyncData.Success) {
localMediaActions.share(localMedia.data)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt
index 94d6653241c..3e0deaf9b34 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt
@@ -9,16 +9,19 @@ package io.element.android.libraries.mediaviewer.impl.viewer
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
data class MediaViewerState(
+ val eventId: EventId?,
val mediaInfo: MediaInfo,
val thumbnailSource: MediaSource?,
val downloadedMedia: AsyncData,
val snackbarMessage: SnackbarMessage?,
- val canDownload: Boolean,
- val canShare: Boolean,
+ val canShowInfo: Boolean,
+ val mediaBottomSheetState: MediaBottomSheetState,
val eventSink: (MediaViewerEvents) -> Unit,
)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt
index 6c7a9fb704f..863c996b9df 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt
@@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer
import android.net.Uri
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.components.media.aWaveForm
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
@@ -17,6 +18,9 @@ import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
+import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirmationState
+import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
open class MediaViewerStateProvider : PreviewParameterProvider {
override val values: Sequence
@@ -30,64 +34,79 @@ open class MediaViewerStateProvider : PreviewParameterProvider
caption = "A caption",
).let {
aMediaViewerState(
- AsyncData.Success(
+ downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
- it,
+ mediaInfo = it,
)
},
aVideoMediaInfo(
- senderName = "Sally Sanderson",
- dateSent = "21 NOV, 2024",
+ senderName = "A very long name so that it will be truncated and will not be displayed on multiple lines",
+ dateSent = "A very very long date that will be truncated and will not be displayed on multiple lines",
caption = "A caption",
).let {
aMediaViewerState(
- AsyncData.Success(
+ downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
- it,
+ mediaInfo = it,
)
},
aPdfMediaInfo().let {
aMediaViewerState(
- AsyncData.Success(
+ downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
- it,
+ mediaInfo = it,
)
},
aMediaViewerState(
- AsyncData.Loading(),
- anApkMediaInfo(),
+ downloadedMedia = AsyncData.Loading(),
+ mediaInfo = anApkMediaInfo(),
),
anApkMediaInfo().let {
aMediaViewerState(
- AsyncData.Success(
+ downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
- it,
+ mediaInfo = it,
)
},
aMediaViewerState(
- AsyncData.Loading(),
- anAudioMediaInfo(),
+ downloadedMedia = AsyncData.Loading(),
+ mediaInfo = anAudioMediaInfo(),
),
anAudioMediaInfo().let {
aMediaViewerState(
- AsyncData.Success(
+ downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
- it,
+ mediaInfo = it,
)
},
anImageMediaInfo().let {
aMediaViewerState(
- AsyncData.Success(
+ downloadedMedia = AsyncData.Success(
+ LocalMedia(Uri.EMPTY, it)
+ ),
+ mediaInfo = it,
+ canShowInfo = false,
+ )
+ },
+ aMediaViewerState(
+ mediaBottomSheetState = aMediaDetailsBottomSheetState(),
+ ),
+ aMediaViewerState(
+ mediaBottomSheetState = aMediaDeleteConfirmationState(),
+ ),
+ anAudioMediaInfo(
+ waveForm = aWaveForm(),
+ ).let {
+ aMediaViewerState(
+ downloadedMedia = AsyncData.Success(
LocalMedia(Uri.EMPTY, it)
),
- it,
- canDownload = false,
- canShare = false,
+ mediaInfo = it,
)
},
)
@@ -96,15 +115,16 @@ open class MediaViewerStateProvider : PreviewParameterProvider
fun aMediaViewerState(
downloadedMedia: AsyncData = AsyncData.Uninitialized,
mediaInfo: MediaInfo = anImageMediaInfo(),
- canDownload: Boolean = true,
- canShare: Boolean = true,
+ canShowInfo: Boolean = true,
+ mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden,
eventSink: (MediaViewerEvents) -> Unit = {},
) = MediaViewerState(
+ eventId = null,
mediaInfo = mediaInfo,
thumbnailSource = null,
downloadedMedia = downloadedMedia,
snackbarMessage = null,
- canDownload = canDownload,
- canShare = canShare,
+ canShowInfo = canShowInfo,
+ mediaBottomSheetState = mediaBottomSheetState,
eventSink = eventSink,
)
diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt
index cdcc6f9fb2f..4a15f95cea0 100644
--- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt
+++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt
@@ -17,8 +17,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -53,6 +51,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.mimetype.MimeTypes
+import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@@ -69,6 +68,9 @@ import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.impl.R
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
+import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet
+import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaView
import io.element.android.libraries.mediaviewer.impl.local.PlayableState
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState
@@ -117,24 +119,59 @@ fun MediaViewerView(
) {
MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is AsyncData.Success,
+ mimeType = state.mediaInfo.mimeType,
senderName = state.mediaInfo.senderName,
dateSent = state.mediaInfo.dateSent,
+ canShowInfo = state.canShowInfo,
onBackClick = onBackClick,
+ onInfoClick = {
+ state.eventSink(MediaViewerEvents.OpenInfo)
+ },
eventSink = state.eventSink
)
MediaViewerBottomBar(
modifier = Modifier.align(Alignment.BottomCenter),
- actionsEnabled = state.downloadedMedia is AsyncData.Success,
- canDownload = state.canDownload,
- canShare = state.canShare,
- mimeType = state.mediaInfo.mimeType,
+ showDivider = state.mediaInfo.mimeType.isMimeTypeVideo(),
caption = state.mediaInfo.caption,
onHeightChange = { bottomPaddingInPixels = it },
- eventSink = state.eventSink
)
}
}
}
+ when (val bottomSheetState = state.mediaBottomSheetState) {
+ MediaBottomSheetState.Hidden -> Unit
+ is MediaBottomSheetState.MediaDetailsBottomSheetState -> {
+ MediaDetailsBottomSheet(
+ state = bottomSheetState,
+ onViewInTimeline = {
+ state.eventSink(MediaViewerEvents.ViewInTimeline(it))
+ },
+ onShare = {
+ state.eventSink(MediaViewerEvents.Share)
+ },
+ onDownload = {
+ state.eventSink(MediaViewerEvents.SaveOnDisk)
+ },
+ onDelete = { eventId ->
+ state.eventSink(MediaViewerEvents.ConfirmDelete(eventId))
+ },
+ onDismiss = {
+ state.eventSink(MediaViewerEvents.CloseBottomSheet)
+ },
+ )
+ }
+ is MediaBottomSheetState.MediaDeleteConfirmationState -> {
+ MediaDeleteConfirmationBottomSheet(
+ state = bottomSheetState,
+ onDelete = {
+ state.eventSink(MediaViewerEvents.Delete(it))
+ },
+ onDismiss = {
+ state.eventSink(MediaViewerEvents.CloseBottomSheet)
+ },
+ )
+ }
+ }
}
@Composable
@@ -276,14 +313,16 @@ private fun rememberShowProgress(downloadedMedia: AsyncData): Boolea
return showProgress
}
-@Suppress("UNUSED_PARAMETER")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MediaViewerTopBar(
actionsEnabled: Boolean,
+ mimeType: String,
senderName: String?,
dateSent: String?,
+ canShowInfo: Boolean,
onBackClick: () -> Unit,
+ onInfoClick: () -> Unit,
eventSink: (MediaViewerEvents) -> Unit,
) {
TopAppBar(
@@ -292,18 +331,20 @@ private fun MediaViewerTopBar(
Column(
modifier = Modifier
.fillMaxWidth()
- .padding(end = 48.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = senderName,
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
)
Text(
text = dateSent,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textPrimary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
)
}
}
@@ -313,20 +354,43 @@ private fun MediaViewerTopBar(
),
navigationIcon = { BackButton(onClick = onBackClick) },
actions = {
- // TODO Add action to open infos.
+ IconButton(
+ enabled = actionsEnabled,
+ onClick = {
+ eventSink(MediaViewerEvents.OpenWith)
+ },
+ ) {
+ when (mimeType) {
+ MimeTypes.Apk -> Icon(
+ resourceId = R.drawable.ic_apk_install,
+ contentDescription = stringResource(id = CommonStrings.common_install_apk_android)
+ )
+ else -> Icon(
+ imageVector = Icons.AutoMirrored.Filled.OpenInNew,
+ contentDescription = stringResource(id = CommonStrings.action_open_with)
+ )
+ }
+ }
+ if (canShowInfo) {
+ IconButton(
+ onClick = onInfoClick,
+ enabled = actionsEnabled,
+ ) {
+ Icon(
+ imageVector = CompoundIcons.Info(),
+ contentDescription = null,
+ )
+ }
+ }
}
)
}
@Composable
private fun MediaViewerBottomBar(
- actionsEnabled: Boolean,
- canDownload: Boolean,
- canShare: Boolean,
- mimeType: String,
caption: String?,
+ showDivider: Boolean,
onHeightChange: (Int) -> Unit,
- eventSink: (MediaViewerEvents) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@@ -337,8 +401,10 @@ private fun MediaViewerBottomBar(
onHeightChange(it.height)
},
) {
- HorizontalDivider()
if (caption != null) {
+ if (showDivider) {
+ HorizontalDivider()
+ }
Text(
modifier = Modifier
.fillMaxWidth()
@@ -349,58 +415,6 @@ private fun MediaViewerBottomBar(
style = ElementTheme.typography.fontBodyLgRegular,
)
}
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(start = 8.dp, end = 8.dp, bottom = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- if (canShare) {
- IconButton(
- enabled = actionsEnabled,
- onClick = {
- eventSink(MediaViewerEvents.Share)
- },
- modifier = Modifier.align(Alignment.CenterVertically)
- ) {
- Icon(
- imageVector = CompoundIcons.ShareAndroid(),
- contentDescription = stringResource(id = CommonStrings.action_share)
- )
- }
- }
- Spacer(modifier = Modifier.weight(1f))
- IconButton(
- enabled = actionsEnabled,
- onClick = {
- eventSink(MediaViewerEvents.OpenWith)
- },
- ) {
- when (mimeType) {
- MimeTypes.Apk -> Icon(
- resourceId = R.drawable.ic_apk_install,
- contentDescription = stringResource(id = CommonStrings.common_install_apk_android)
- )
- else -> Icon(
- imageVector = Icons.AutoMirrored.Filled.OpenInNew,
- contentDescription = stringResource(id = CommonStrings.action_open_with)
- )
- }
- }
- if (canDownload) {
- IconButton(
- enabled = actionsEnabled,
- onClick = {
- eventSink(MediaViewerEvents.SaveOnDisk)
- },
- ) {
- Icon(
- imageVector = CompoundIcons.Download(),
- contentDescription = stringResource(id = CommonStrings.action_save),
- )
- }
- }
- }
}
}
diff --git a/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 00000000000..a987f8957c5
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,20 @@
+
+
+ "Tento soubor bude odstraněn z místnosti a členové k němu nebudou mít přístup."
+ "Smazat soubor?"
+ "Zde se zobrazí dokumenty, zvukové soubory a hlasové zprávy nahrané do této místnosti."
+ "Zatím nebyly nahrány žádné soubory"
+ "Načítání souborů…"
+ "Načítání médií…"
+ "Soubory"
+ "Média"
+ "Obrázky a videa nahraná do této místnosti budou zobrazeny zde."
+ "Zatím nebyla nahrána žádná média"
+ "Média a soubory"
+ "Formát souboru"
+ "Název souboru"
+ "Tento soubor bude odstraněn z místnosti a členové k němu nebudou mít přístup."
+ "Smazat soubor?"
+ "Nahrál(a)"
+ "Nahráno"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml
new file mode 100644
index 00000000000..813d358ec04
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-de/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "Dateien werden geladen…"
+ "Medien werden geladen…"
+ "Dateien"
+ "Medien"
+ "In diesen Chatroom hochgeladene Bilder und Videos werden hier angezeigt."
+ "Noch keine Medien hochgeladen"
+ "Medien und Dateien"
+ "Dateiformat"
+ "Dateiname"
+ "Hochgeladen von"
+ "Hochgeladen am"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values-el/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-el/translations.xml
new file mode 100644
index 00000000000..8452eb9158d
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-el/translations.xml
@@ -0,0 +1,18 @@
+
+
+ "Αυτό το αρχείο θα αφαιρεθεί από την αίθουσα και τα μέλη δεν θα έχουν πρόσβαση σε αυτό."
+ "Διαγραφή αρχείου;"
+ "Φόρτωση αρχείων…"
+ "Φόρτωση πολυμέσων…"
+ "Αρχεία"
+ "Πολυμέσα"
+ "Εικόνες και βίντεο που μεταφορτώνονται σε αυτό το δωμάτιο θα εμφανίζονται εδώ."
+ "Δεν έχουν μεταφορτωθεί ακόμα πολυμέσα"
+ "Πολυμέσα και αρχεία"
+ "Μορφή αρχείου"
+ "Όνομα αρχείου"
+ "Αυτό το αρχείο θα αφαιρεθεί από το δωμάτιο και τα μέλη δεν θα έχουν πρόσβαση σε αυτό."
+ "Διαγραφή αρχείου;"
+ "Μεταφορτώθηκε από"
+ "Μεταφορτώθηκε στις"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml
new file mode 100644
index 00000000000..85285375e15
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-et/translations.xml
@@ -0,0 +1,18 @@
+
+
+ "Järgnevaga eemaldame selle faili jututoast ka tema liikmed enam ei pääse failile ligi."
+ "Kas kustutame faili?"
+ "Laadime faile…"
+ "Laadime meediat…"
+ "Failid"
+ "Meedia"
+ "Antud jututuppa üleslaaditud pildid ja videod kuvatakse siin."
+ "Mitte keegi pole veel meediat üles laadinud"
+ "Meedia ja failid"
+ "Failivorming"
+ "Failinimi"
+ "Järgnevaga eemaldame selle faili jututoast ja tema liikmed enam ei pääse failile ligi."
+ "Kas kustutame faili?"
+ "Üleslaadija"
+ "Üleslaaditud"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml
new file mode 100644
index 00000000000..43cee7b95f0
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-fi/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "Ladataan tiedostoja…"
+ "Ladataan mediaa…"
+ "Tiedostot"
+ "Media"
+ "Tähän huoneeseen lähetetyt kuvat ja videot näytetään täällä."
+ "Mediaa ei ole vielä lähetetty"
+ "Media ja tiedostot"
+ "Tiedostomuoto"
+ "Tiedostonimi"
+ "Lähettäjä:"
+ "Lähetetty"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 00000000000..aeaec0a9b02
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,20 @@
+
+
+ "Ce fichier sera supprimé du salon et les membres n’y auront plus accès."
+ "Supprimer le fichier ?"
+ "Les documents, les fichiers audio et les messages vocaux envoyés dans ce salon seront affichés ici."
+ "Aucun fichier n’a encore été envoyé"
+ "Chargement des fichiers…"
+ "Chargement des médias…"
+ "Fichiers"
+ "Média"
+ "Les images et vidéos envoyées dans ce salon seront affichées ici."
+ "Aucun média n’a encore été envoyé dans ce salon"
+ "Médias et fichiers"
+ "Format du fichier"
+ "Nom du fichier"
+ "Ce fichier sera supprimé du salon et les membres n’y auront plus accès."
+ "Supprimer le fichier ?"
+ "Envoyé par"
+ "Envoyé le"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml
new file mode 100644
index 00000000000..087ab1b4c4d
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-hu/translations.xml
@@ -0,0 +1,20 @@
+
+
+ "Ez a fájl el lesz távolítva a szobából, és a tagok nem férhetnek hozzá."
+ "Törli a fájlt?"
+ "A szobába feltöltött dokumentumok, hangfájlok és hangüzenetek itt jelennek meg."
+ "Még nincsenek fájlok feltöltve"
+ "Fájlok betöltése…"
+ "Média betöltése…"
+ "Fájlok"
+ "Média"
+ "Az ebbe a szobába feltöltött képek és videók itt jelennek meg."
+ "Még nincs feltöltött média"
+ "Média és fájlok"
+ "Fájlformátum"
+ "Fájlnév"
+ "Ez a fájl el lesz távolítva a szobából, és a tagok nem férhetnek hozzá."
+ "Törli a fájlt?"
+ "Feltöltötte:"
+ "Feltöltve:"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml
new file mode 100644
index 00000000000..237209e0d7c
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-it/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "File"
+ "Contenuti multimediali"
+ "Le immagini e i video caricati in questa stanza verranno mostrati qui."
+ "Nessun file multimediale ancora caricato"
+ "File e contenuti multimediali"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml b/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml
new file mode 100644
index 00000000000..cc0f2e573f9
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values-ru/translations.xml
@@ -0,0 +1,18 @@
+
+
+ "Этот файл будет удален из комнаты и участники не будут иметь к нему доступ."
+ "Удалить файл?"
+ "Загрузка файлов…"
+ "Загрузка медиа…"
+ "Файлы"
+ "Медиа"
+ "Здесь будут показаны изображения и видео, загруженные в данную комнату."
+ "Пока что нет загруженных медиафайлов"
+ "Медиа и файлы"
+ "Формат файла"
+ "Имя файла"
+ "Этот файл будет удален из комнаты и у участников не будет к нему доступа."
+ "Удалить файл?"
+ "Загружено"
+ "Загружено на"
+
diff --git a/libraries/mediaviewer/impl/src/main/res/values/localazy.xml b/libraries/mediaviewer/impl/src/main/res/values/localazy.xml
new file mode 100644
index 00000000000..6072c56db88
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,20 @@
+
+
+ "This file will be removed from the room and members won’t have access to it."
+ "Delete file?"
+ "Documents, audio files, and voice messages uploaded to this room will be shown here."
+ "No files uploaded yet"
+ "Loading files…"
+ "Loading media…"
+ "Files"
+ "Media"
+ "Images and videos uploaded to this room will be shown here."
+ "No media uploaded yet"
+ "Media and files"
+ "File format"
+ "File name"
+ "This file will be removed from the room and members won’t have access to it."
+ "Delete file?"
+ "Uploaded by"
+ "Uploaded on"
+
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt
new file mode 100644
index 00000000000..cc652f21ec8
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.details
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EnsureNeverCalled
+import io.element.android.tests.testutils.EnsureNeverCalledWithParam
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.ensureCalledOnce
+import io.element.android.tests.testutils.ensureCalledOnceWithParam
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MediaDeleteConfirmationBottomSheetTest {
+ @get:Rule
+ val rule = createAndroidComposeRule()
+
+ @Test
+ fun `clicking on Cancel invokes expected callback`() {
+ val state = aMediaDeleteConfirmationState()
+ ensureCalledOnce { callback ->
+ rule.setMediaDeleteConfirmationBottomSheet(
+ state = state,
+ onDismiss = callback,
+ )
+ rule.clickOn(CommonStrings.action_cancel)
+ }
+ }
+
+ @Test
+ fun `clicking on Remove invokes expected callback`() {
+ val state = aMediaDeleteConfirmationState()
+ ensureCalledOnceWithParam(state.eventId) { callback ->
+ rule.setMediaDeleteConfirmationBottomSheet(
+ state = state,
+ onDelete = callback,
+ )
+ rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists()
+ rule.clickOn(CommonStrings.action_remove)
+ }
+ }
+}
+
+private fun AndroidComposeTestRule.setMediaDeleteConfirmationBottomSheet(
+ state: MediaBottomSheetState.MediaDeleteConfirmationState,
+ onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
+ onDismiss: () -> Unit = EnsureNeverCalled(),
+) {
+ setContent {
+ MediaDeleteConfirmationBottomSheet(
+ state = state,
+ onDelete = onDelete,
+ onDismiss = onDismiss,
+ )
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt
new file mode 100644
index 00000000000..2a19be26f3a
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.details
+
+import androidx.activity.ComponentActivity
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.ui.strings.CommonStrings
+import io.element.android.tests.testutils.EnsureNeverCalled
+import io.element.android.tests.testutils.EnsureNeverCalledWithParam
+import io.element.android.tests.testutils.clickOn
+import io.element.android.tests.testutils.ensureCalledOnceWithParam
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+class MediaDetailsBottomSheetTest {
+ @get:Rule
+ val rule = createAndroidComposeRule()
+
+ @Test
+ fun `clicking on View in timeline invokes expected callback`() {
+ val state = aMediaDetailsBottomSheetState()
+ ensureCalledOnceWithParam(state.eventId) { callback ->
+ rule.setMediaDetailsBottomSheet(
+ state = state,
+ onViewInTimeline = callback,
+ )
+ rule.clickOn(CommonStrings.action_view_in_timeline)
+ }
+ }
+
+ @Test
+ fun `clicking on Share invokes expected callback`() {
+ val state = aMediaDetailsBottomSheetState()
+ ensureCalledOnceWithParam(state.eventId) { callback ->
+ rule.setMediaDetailsBottomSheet(
+ state = state,
+ onShare = callback,
+ )
+ rule.clickOn(CommonStrings.action_share)
+ }
+ }
+
+ @Test
+ fun `clicking on Save invokes expected callback`() {
+ val state = aMediaDetailsBottomSheetState()
+ ensureCalledOnceWithParam(state.eventId) { callback ->
+ rule.setMediaDetailsBottomSheet(
+ state = state,
+ onDownload = callback,
+ )
+ rule.clickOn(CommonStrings.action_save)
+ }
+ }
+
+ @Config(qualifiers = "h1024dp")
+ @Test
+ fun `clicking on Remove invokes expected callback`() {
+ val state = aMediaDetailsBottomSheetState()
+ ensureCalledOnceWithParam(state.eventId) { callback ->
+ rule.setMediaDetailsBottomSheet(
+ state = state,
+ onDelete = callback,
+ )
+ rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertExists()
+ rule.clickOn(CommonStrings.action_remove)
+ }
+ }
+
+ @Config(qualifiers = "h1024dp")
+ @Test
+ fun `Remove is not present if canDelete is false`() {
+ val state = aMediaDetailsBottomSheetState(
+ canDelete = false,
+ )
+ rule.setMediaDetailsBottomSheet(
+ state = state,
+ )
+ rule.onNodeWithText(rule.activity.getString(CommonStrings.action_remove)).assertDoesNotExist()
+ }
+}
+
+private fun AndroidComposeTestRule.setMediaDetailsBottomSheet(
+ state: MediaBottomSheetState.MediaDetailsBottomSheetState,
+ onViewInTimeline: (EventId) -> Unit = EnsureNeverCalledWithParam(),
+ onShare: (EventId) -> Unit = EnsureNeverCalledWithParam(),
+ onDownload: (EventId) -> Unit = EnsureNeverCalledWithParam(),
+ onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(),
+ onDismiss: () -> Unit = EnsureNeverCalled(),
+) {
+ setContent {
+ MediaDetailsBottomSheet(
+ state = state,
+ onViewInTimeline = onViewInTimeline,
+ onShare = onShare,
+ onDownload = onDownload,
+ onDelete = onDelete,
+ onDismiss = onDismiss,
+ )
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt
new file mode 100644
index 00000000000..c03ebc37c4c
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+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.dateformatter.test.FakeDateFormatter
+import io.element.android.libraries.matrix.api.media.AudioDetails
+import io.element.android.libraries.matrix.api.media.AudioInfo
+import io.element.android.libraries.matrix.api.media.FileInfo
+import io.element.android.libraries.matrix.api.media.ImageInfo
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.api.media.VideoInfo
+import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
+import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
+import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
+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.LegacyCallInviteContent
+import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
+import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
+import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
+import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
+import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
+import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
+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_UNIQUE_ID
+import io.element.android.libraries.matrix.test.A_USER_ID
+import io.element.android.libraries.matrix.test.timeline.aMessageContent
+import io.element.android.libraries.matrix.test.timeline.aPollContent
+import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
+import io.element.android.libraries.matrix.test.timeline.aStickerContent
+import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
+import io.element.android.libraries.mediaviewer.api.MediaInfo
+import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+import org.junit.Test
+import kotlin.time.Duration.Companion.seconds
+
+class DefaultEventItemFactoryTest {
+ @Test
+ fun `create check all null cases`() {
+ val factory = createEventItemFactory()
+ val contents = listOf(
+ CallNotifyContent,
+ FailedToParseMessageLikeContent("", ""),
+ FailedToParseStateContent("", "", ""),
+ LegacyCallInviteContent,
+ aPollContent(),
+ aProfileChangeMessageContent(),
+ RedactedContent,
+ RoomMembershipContent(
+ userId = A_USER_ID,
+ userDisplayName = null,
+ change = null,
+ ),
+ StateContent("", OtherState.RoomCreate),
+ aStickerContent(
+ info = ImageInfo(
+ width = null,
+ height = null,
+ mimetype = null,
+ size = null,
+ thumbnailInfo = null,
+ thumbnailSource = null,
+ blurhash = null,
+ ),
+ mediaSource = MediaSource("")
+ ),
+ UnableToDecryptContent(UnableToDecryptContent.Data.Unknown),
+ UnknownContent,
+ )
+ contents.forEach {
+ val result = factory.create(
+ MatrixTimelineItem.Event(
+ uniqueId = A_UNIQUE_ID,
+ event = anEventTimelineItem(
+ content = it
+ )
+ )
+ )
+ assertThat(result).isNull()
+ }
+ }
+
+ @Test
+ fun `create MessageContent check all null cases`() {
+ val factory = createEventItemFactory()
+ val messageTypes = listOf(
+ EmoteMessageType("", null),
+ NoticeMessageType("", null),
+ OtherMessageType("", ""),
+ LocationMessageType("", "", null),
+ TextMessageType("", null)
+ )
+ messageTypes.forEach {
+ val result = factory.create(
+ MatrixTimelineItem.Event(
+ uniqueId = A_UNIQUE_ID,
+ event = anEventTimelineItem(
+ content = aMessageContent(
+ messageType = it
+ )
+ )
+ )
+ )
+ assertThat(result).isNull()
+ }
+ }
+
+ @Test
+ fun `create for FileMessageType`() {
+ val factory = createEventItemFactory()
+ val result = factory.create(
+ MatrixTimelineItem.Event(
+ uniqueId = A_UNIQUE_ID,
+ event = anEventTimelineItem(
+ content = aMessageContent(
+ messageType = FileMessageType(
+ filename = "filename.apk",
+ caption = "caption",
+ formattedCaption = null,
+ source = MediaSource(""),
+ info = FileInfo(
+ mimetype = MimeTypes.Apk,
+ size = 123L,
+ thumbnailInfo = null,
+ thumbnailSource = null,
+ )
+ )
+ )
+ )
+ )
+ )
+ assertThat(result).isEqualTo(
+ MediaItem.File(
+ id = A_UNIQUE_ID,
+ eventId = AN_EVENT_ID,
+ mediaInfo = MediaInfo(
+ mimeType = MimeTypes.Apk,
+ filename = "filename.apk",
+ caption = "caption",
+ formattedFileSize = "123 Bytes",
+ fileExtension = "apk",
+ senderId = A_USER_ID,
+ senderName = "alice",
+ senderAvatar = null,
+ dateSent = "0 Day false",
+ dateSentFull = "0 Full false",
+ waveform = null,
+ ),
+ mediaSource = MediaSource(""),
+ )
+ )
+ }
+
+ @Test
+ fun `create for ImageMessageType`() {
+ val factory = createEventItemFactory()
+ val result = factory.create(
+ MatrixTimelineItem.Event(
+ uniqueId = A_UNIQUE_ID,
+ event = anEventTimelineItem(
+ content = aMessageContent(
+ messageType = ImageMessageType(
+ filename = "filename.jpg",
+ caption = "caption",
+ formattedCaption = null,
+ source = MediaSource(""),
+ info = ImageInfo(
+ mimetype = MimeTypes.Jpeg,
+ size = 123L,
+ thumbnailInfo = null,
+ thumbnailSource = null,
+ height = 1L,
+ width = 2L,
+ blurhash = null,
+ )
+ )
+ )
+ )
+ )
+ )
+ assertThat(result).isEqualTo(
+ MediaItem.Image(
+ id = A_UNIQUE_ID,
+ eventId = AN_EVENT_ID,
+ mediaInfo = MediaInfo(
+ mimeType = MimeTypes.Jpeg,
+ filename = "filename.jpg",
+ caption = "caption",
+ formattedFileSize = "123 Bytes",
+ fileExtension = "jpg",
+ senderId = A_USER_ID,
+ senderName = "alice",
+ senderAvatar = null,
+ dateSent = "0 Day false",
+ dateSentFull = "0 Full false",
+ waveform = null,
+ ),
+ mediaSource = MediaSource(""),
+ thumbnailSource = null,
+ )
+ )
+ }
+
+ @Test
+ fun `create for AudioMessageType`() {
+ val factory = createEventItemFactory()
+ val result = factory.create(
+ MatrixTimelineItem.Event(
+ uniqueId = A_UNIQUE_ID,
+ event = anEventTimelineItem(
+ content = aMessageContent(
+ messageType = AudioMessageType(
+ filename = "filename.mp3",
+ caption = "caption",
+ formattedCaption = null,
+ source = MediaSource(""),
+ info = AudioInfo(
+ mimetype = MimeTypes.Mp3,
+ size = 123L,
+ duration = 456.seconds,
+ )
+ )
+ )
+ )
+ )
+ )
+ assertThat(result).isEqualTo(
+ MediaItem.Audio(
+ id = A_UNIQUE_ID,
+ eventId = AN_EVENT_ID,
+ mediaInfo = MediaInfo(
+ mimeType = MimeTypes.Mp3,
+ filename = "filename.mp3",
+ caption = "caption",
+ formattedFileSize = "123 Bytes",
+ fileExtension = "mp3",
+ senderId = A_USER_ID,
+ senderName = "alice",
+ senderAvatar = null,
+ dateSent = "0 Day false",
+ dateSentFull = "0 Full false",
+ waveform = null,
+ ),
+ mediaSource = MediaSource(""),
+ )
+ )
+ }
+
+ @Test
+ fun `create for VideoMessageType`() {
+ val factory = createEventItemFactory()
+ val result = factory.create(
+ MatrixTimelineItem.Event(
+ uniqueId = A_UNIQUE_ID,
+ event = anEventTimelineItem(
+ content = aMessageContent(
+ messageType = VideoMessageType(
+ filename = "filename.mp4",
+ caption = "caption",
+ formattedCaption = null,
+ source = MediaSource(""),
+ info = VideoInfo(
+ mimetype = MimeTypes.Mp4,
+ size = 123L,
+ thumbnailInfo = null,
+ duration = 123.seconds,
+ height = 1L,
+ width = 2L,
+ thumbnailSource = null,
+ blurhash = null
+ )
+ )
+ )
+ )
+ )
+ )
+ assertThat(result).isEqualTo(
+ MediaItem.Video(
+ id = A_UNIQUE_ID,
+ eventId = AN_EVENT_ID,
+ mediaInfo = MediaInfo(
+ mimeType = MimeTypes.Mp4,
+ filename = "filename.mp4",
+ caption = "caption",
+ formattedFileSize = "123 Bytes",
+ fileExtension = "mp4",
+ senderId = A_USER_ID,
+ senderName = "alice",
+ senderAvatar = null,
+ dateSent = "0 Day false",
+ dateSentFull = "0 Full false",
+ waveform = null,
+ ),
+ mediaSource = MediaSource(""),
+ thumbnailSource = null,
+ duration = "2:03",
+ )
+ )
+ }
+
+ @Test
+ fun `create for VoiceMessageType`() {
+ val factory = createEventItemFactory()
+ val result = factory.create(
+ MatrixTimelineItem.Event(
+ uniqueId = A_UNIQUE_ID,
+ event = anEventTimelineItem(
+ content = aMessageContent(
+ messageType = VoiceMessageType(
+ filename = "filename.ogg",
+ caption = "caption",
+ formattedCaption = null,
+ source = MediaSource(""),
+ info = AudioInfo(
+ mimetype = MimeTypes.Ogg,
+ size = 123L,
+ duration = 456.seconds,
+ ),
+ details = AudioDetails(
+ duration = 456.seconds,
+ waveform = persistentListOf(1f, 2f),
+ )
+ )
+ )
+ )
+ )
+ )
+ assertThat(result).isEqualTo(
+ MediaItem.Voice(
+ id = A_UNIQUE_ID,
+ eventId = AN_EVENT_ID,
+ mediaInfo = MediaInfo(
+ mimeType = MimeTypes.Ogg,
+ filename = "filename.ogg",
+ caption = "caption",
+ formattedFileSize = "123 Bytes",
+ fileExtension = "ogg",
+ senderId = A_USER_ID,
+ senderName = "alice",
+ senderAvatar = null,
+ dateSent = "0 Day false",
+ dateSentFull = "0 Full false",
+ waveform = listOf(1f, 2f).toImmutableList(),
+ ),
+ mediaSource = MediaSource(""),
+ duration = "7:36",
+ waveform = listOf(1f, 2f).toImmutableList(),
+ )
+ )
+ }
+
+ @Test
+ fun `create for StickerMessageType`() {
+ val factory = createEventItemFactory()
+ val result = factory.create(
+ MatrixTimelineItem.Event(
+ uniqueId = A_UNIQUE_ID,
+ event = anEventTimelineItem(
+ content = aMessageContent(
+ messageType = StickerMessageType(
+ filename = "filename.gif",
+ caption = "caption",
+ formattedCaption = null,
+ source = MediaSource(""),
+ info = ImageInfo(
+ mimetype = MimeTypes.Gif,
+ size = 123L,
+ thumbnailInfo = null,
+ thumbnailSource = null,
+ height = 1L,
+ width = 2L,
+ blurhash = null,
+ )
+ )
+ )
+ )
+ )
+ )
+ assertThat(result).isEqualTo(
+ MediaItem.Image(
+ id = A_UNIQUE_ID,
+ eventId = AN_EVENT_ID,
+ mediaInfo = MediaInfo(
+ mimeType = MimeTypes.Gif,
+ filename = "filename.gif",
+ caption = "caption",
+ formattedFileSize = "123 Bytes",
+ fileExtension = "gif",
+ senderId = A_USER_ID,
+ senderName = "alice",
+ senderAvatar = null,
+ dateSent = "0 Day false",
+ dateSentFull = "0 Full false",
+ waveform = null,
+ ),
+ mediaSource = MediaSource(""),
+ thumbnailSource = null,
+ )
+ )
+ }
+}
+
+private fun createEventItemFactory() = EventItemFactory(
+ fileSizeFormatter = FakeFileSizeFormatter(),
+ fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
+ dateFormatter = FakeDateFormatter(),
+)
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryNavigator.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryNavigator.kt
new file mode 100644
index 00000000000..6633fcbce16
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/FakeMediaGalleryNavigator.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeMediaGalleryNavigator(
+ private val onViewInTimelineClickLambda: (EventId) -> Unit = { lambdaError() }
+) : MediaGalleryNavigator {
+ override fun onViewInTimelineClick(eventId: EventId) {
+ onViewInTimelineClickLambda(eventId)
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt
new file mode 100644
index 00000000000..8eeaef976c6
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import android.net.Uri
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
+import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.test.AN_EVENT_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_ID_2
+import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
+import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.libraries.matrix.test.timeline.FakeTimeline
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
+import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
+import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
+import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
+import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.test
+import io.element.android.tests.testutils.testCoroutineDispatchers
+import 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
+
+class MediaGalleryPresenterTest {
+ @get:Rule
+ val warmUpRule = WarmUpRule()
+
+ private val mockMediaUri: Uri = mockk("localMediaUri")
+ private val localMediaFactory = FakeLocalMediaFactory(mockMediaUri)
+
+ @Test
+ fun `present - initial state`() = runTest {
+ val onViewInTimelineClickLambda = lambdaRecorder { }
+ val navigator = FakeMediaGalleryNavigator(
+ onViewInTimelineClickLambda = onViewInTimelineClickLambda,
+ )
+ val presenter = createMediaGalleryPresenter(
+ navigator = navigator,
+ room = FakeMatrixRoom(
+ displayName = A_ROOM_NAME,
+ mediaTimelineResult = { Result.success(FakeTimeline()) },
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
+ assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ assertThat(initialState.roomName).isEqualTo(A_ROOM_NAME)
+ assertThat(initialState.groupedMediaItems.dataOrNull()).isEqualTo(
+ GroupedMediaItems(
+ imageAndVideoItems = persistentListOf(),
+ fileItems = persistentListOf(),
+ )
+ )
+ assertThat(initialState.snackbarMessage).isNull()
+ }
+ }
+
+ @Test
+ fun `present - change mode`() = runTest {
+ val onViewInTimelineClickLambda = lambdaRecorder { }
+ val navigator = FakeMediaGalleryNavigator(
+ onViewInTimelineClickLambda = onViewInTimelineClickLambda,
+ )
+ val presenter = createMediaGalleryPresenter(
+ navigator = navigator,
+ room = FakeMatrixRoom(
+ displayName = A_ROOM_NAME,
+ mediaTimelineResult = { Result.success(FakeTimeline()) },
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
+ initialState.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Files))
+ val state = awaitItem()
+ assertThat(state.mode).isEqualTo(MediaGalleryMode.Files)
+ state.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Images))
+ val imageModeState = awaitItem()
+ assertThat(imageModeState.mode).isEqualTo(MediaGalleryMode.Images)
+ }
+ }
+
+ @Test
+ fun `present - bottom sheet state - own message and can delete own`() = runTest {
+ `present - bottom sheet state - own message`(canDeleteOwn = true)
+ }
+
+ @Test
+ fun `present - bottom sheet state - own message and cannot delete own`() = runTest {
+ `present - bottom sheet state - own message`(canDeleteOwn = false)
+ }
+
+ private suspend fun TestScope.`present - bottom sheet state - own message`(canDeleteOwn: Boolean) {
+ val presenter = createMediaGalleryPresenter(
+ room = FakeMatrixRoom(
+ sessionId = A_USER_ID,
+ displayName = A_ROOM_NAME,
+ mediaTimelineResult = { Result.success(FakeTimeline()) },
+ canRedactOwnResult = { Result.success(canDeleteOwn) }
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ val item = aMediaItemImage(
+ eventId = AN_EVENT_ID,
+ senderId = A_USER_ID,
+ )
+ initialState.eventSink(MediaGalleryEvents.OpenInfo(item))
+ val state = awaitItem()
+ assertThat(state.mediaBottomSheetState).isEqualTo(
+ MediaBottomSheetState.MediaDetailsBottomSheetState(
+ eventId = AN_EVENT_ID,
+ canDelete = canDeleteOwn,
+ mediaInfo = item.mediaInfo,
+ thumbnailSource = item.mediaSource,
+ )
+ )
+ // Close the bottom sheet
+ state.eventSink(MediaGalleryEvents.CloseBottomSheet)
+ val closedState = awaitItem()
+ assertThat(closedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ }
+ }
+
+ @Test
+ fun `present - bottom sheet state - other message and can delete other`() = runTest {
+ `present - bottom sheet state - other message`(canDeleteOther = true)
+ }
+
+ @Test
+ fun `present - bottom sheet state - other message and cannot delete other`() = runTest {
+ `present - bottom sheet state - other message`(canDeleteOther = false)
+ }
+
+ private suspend fun TestScope.`present - bottom sheet state - other message`(canDeleteOther: Boolean) {
+ val presenter = createMediaGalleryPresenter(
+ room = FakeMatrixRoom(
+ sessionId = A_USER_ID,
+ displayName = A_ROOM_NAME,
+ mediaTimelineResult = { Result.success(FakeTimeline()) },
+ canRedactOtherResult = { Result.success(canDeleteOther) }
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ val item = aMediaItemImage(
+ eventId = AN_EVENT_ID,
+ senderId = A_USER_ID_2,
+ )
+ initialState.eventSink(MediaGalleryEvents.OpenInfo(item))
+ val state = awaitItem()
+ assertThat(state.mediaBottomSheetState).isEqualTo(
+ MediaBottomSheetState.MediaDetailsBottomSheetState(
+ eventId = AN_EVENT_ID,
+ canDelete = canDeleteOther,
+ mediaInfo = item.mediaInfo,
+ thumbnailSource = item.mediaSource,
+ )
+ )
+ // Close the bottom sheet
+ state.eventSink(MediaGalleryEvents.CloseBottomSheet)
+ val closedState = awaitItem()
+ assertThat(closedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ }
+ }
+
+ @Test
+ fun `present - delete bottom sheet`() = runTest {
+ val presenter = createMediaGalleryPresenter(
+ room = FakeMatrixRoom(
+ displayName = A_ROOM_NAME,
+ mediaTimelineResult = { Result.success(FakeTimeline()) },
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ // Delete bottom sheet
+ val item = aMediaItemImage()
+ initialState.eventSink(MediaGalleryEvents.ConfirmDelete(AN_EVENT_ID, item.mediaInfo, item.thumbnailSource))
+ val deleteState = awaitItem()
+ assertThat(deleteState.mediaBottomSheetState).isEqualTo(
+ MediaBottomSheetState.MediaDeleteConfirmationState(
+ eventId = AN_EVENT_ID,
+ mediaInfo = item.mediaInfo,
+ thumbnailSource = item.thumbnailSource,
+ )
+ )
+ // Close the bottom sheet
+ deleteState.eventSink(MediaGalleryEvents.CloseBottomSheet)
+ val deleteClosedState = awaitItem()
+ assertThat(deleteClosedState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ }
+ }
+
+ @Test
+ fun `present - view in timeline invokes the navigator`() = runTest {
+ val onViewInTimelineClickLambda = lambdaRecorder { }
+ val navigator = FakeMediaGalleryNavigator(
+ onViewInTimelineClickLambda = onViewInTimelineClickLambda,
+ )
+ val presenter = createMediaGalleryPresenter(
+ room = FakeMatrixRoom(
+ mediaTimelineResult = { Result.success(FakeTimeline()) },
+ ),
+ navigator = navigator,
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ initialState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID))
+ onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
+ }
+ }
+
+ private fun TestScope.createMediaGalleryPresenter(
+ matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
+ localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
+ snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
+ navigator: MediaGalleryNavigator = FakeMediaGalleryNavigator(),
+ room: MatrixRoom = FakeMatrixRoom(
+ liveTimeline = FakeTimeline(),
+ ),
+ ): MediaGalleryPresenter {
+ return MediaGalleryPresenter(
+ navigator = navigator,
+ room = room,
+ timelineMediaItemsFactory = TimelineMediaItemsFactory(
+ dispatchers = testCoroutineDispatchers(),
+ virtualItemFactory = VirtualItemFactory(
+ dateFormatter = FakeDateFormatter(),
+ ),
+ eventItemFactory = EventItemFactory(
+ fileSizeFormatter = FakeFileSizeFormatter(),
+ fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
+ dateFormatter = FakeDateFormatter(),
+ ),
+ ),
+ localMediaFactory = localMediaFactory,
+ mediaLoader = matrixMediaLoader,
+ localMediaActions = localMediaActions,
+ snackbarDispatcher = snackbarDispatcher,
+ mediaItemsPostProcessor = MediaItemsPostProcessor(),
+ )
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt
new file mode 100644
index 00000000000..934ed860af8
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.gallery
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.core.UniqueId
+import io.element.android.libraries.matrix.test.AN_EXCEPTION
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemAudio
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemDateSeparator
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
+import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice
+import kotlinx.collections.immutable.toImmutableList
+import org.junit.Test
+
+class MediaItemsPostProcessorTest {
+ private val file1 = aMediaItemFile(id = UniqueId("1"))
+ private val file2 = aMediaItemFile(id = UniqueId("2"))
+ private val file3 = aMediaItemFile(id = UniqueId("3"))
+ private val audio1 = aMediaItemAudio(id = UniqueId("1"))
+ private val audio2 = aMediaItemAudio(id = UniqueId("2"))
+ private val audio3 = aMediaItemAudio(id = UniqueId("3"))
+ private val voice1 = aMediaItemVoice(id = UniqueId("1"))
+ private val voice2 = aMediaItemVoice(id = UniqueId("2"))
+ private val voice3 = aMediaItemVoice(id = UniqueId("3"))
+ private val image1 = aMediaItemImage(id = UniqueId("1"))
+ private val image2 = aMediaItemImage(id = UniqueId("2"))
+ private val image3 = aMediaItemImage(id = UniqueId("3"))
+ private val video1 = aMediaItemVideo(id = UniqueId("1"))
+ private val video2 = aMediaItemVideo(id = UniqueId("2"))
+ private val video3 = aMediaItemVideo(id = UniqueId("3"))
+ private val date1 = aMediaItemDateSeparator(id = UniqueId("1"))
+ private val date2 = aMediaItemDateSeparator(id = UniqueId("2"))
+ private val date3 = aMediaItemDateSeparator(id = UniqueId("3"))
+ private val loading1 = aMediaItemLoadingIndicator(id = UniqueId("1"))
+
+ @Test
+ fun `process Uninitialized`() {
+ val sut = MediaItemsPostProcessor()
+ val result = sut.process(AsyncData.Uninitialized)
+ assertThat(result).isEqualTo(AsyncData.Uninitialized)
+ }
+
+ @Test
+ fun `process Loading`() {
+ val sut = MediaItemsPostProcessor()
+ val result = sut.process(AsyncData.Loading())
+ assertThat(result).isEqualTo(AsyncData.Loading())
+ }
+
+ @Test
+ fun `process Failure`() {
+ val sut = MediaItemsPostProcessor()
+ val result = sut.process(AsyncData.Failure(AN_EXCEPTION))
+ assertThat(result).isEqualTo(AsyncData.Failure(AN_EXCEPTION))
+ }
+
+ @Test
+ fun `process Empty`() {
+ test(
+ mediaItems = listOf(),
+ expectedImageAndVideoItems = emptyList(),
+ expectedFileItems = emptyList(),
+ )
+ }
+
+ @Test
+ fun `process will reorder files`() {
+ test(
+ mediaItems = listOf(
+ audio1,
+ file3,
+ file2,
+ file1,
+ date1,
+ ),
+ expectedImageAndVideoItems = emptyList(),
+ expectedFileItems = listOf(
+ date1,
+ audio1,
+ file3,
+ file2,
+ file1,
+ ),
+ )
+ }
+
+ @Test
+ fun `process will reorder images`() {
+ test(
+ mediaItems = listOf(
+ image3,
+ image2,
+ image1,
+ date1,
+ ),
+ expectedImageAndVideoItems = listOf(
+ date1,
+ image3,
+ image2,
+ image1,
+ ),
+ expectedFileItems = emptyList(),
+ )
+ }
+
+ @Test
+ fun `process will split images, videos and files`() {
+ test(
+ mediaItems = listOf(
+ audio1,
+ file1,
+ image1,
+ video1,
+ date1,
+ ),
+ expectedImageAndVideoItems = listOf(
+ date1,
+ image1,
+ video1,
+ ),
+ expectedFileItems = listOf(
+ date1,
+ audio1,
+ file1,
+ ),
+ )
+ }
+
+ @Test
+ fun `process will skip date if there is no items`() {
+ test(
+ mediaItems = listOf(
+ date1,
+ date2,
+ date3,
+ ),
+ expectedImageAndVideoItems = emptyList(),
+ expectedFileItems = emptyList(),
+ )
+ }
+
+ @Test
+ fun `process will add the loading indicator to both list`() {
+ test(
+ mediaItems = listOf(
+ loading1,
+ ),
+ expectedImageAndVideoItems = listOf(
+ loading1,
+ ),
+ expectedFileItems = listOf(
+ loading1,
+ ),
+ )
+ }
+
+ @Test
+ fun `process will handle complex case`() {
+ test(
+ mediaItems = listOf(
+ file3,
+ date3,
+ video3,
+ video2,
+ date2,
+ voice3,
+ voice2,
+ voice1,
+ audio3,
+ audio2,
+ audio1,
+ file1,
+ image1,
+ video1,
+ date1,
+ loading1,
+ ),
+ expectedImageAndVideoItems = listOf(
+ date2,
+ video3,
+ video2,
+ date1,
+ image1,
+ video1,
+ loading1,
+ ),
+ expectedFileItems = listOf(
+ date3,
+ file3,
+ date1,
+ voice3,
+ voice2,
+ voice1,
+ audio3,
+ audio2,
+ audio1,
+ file1,
+ loading1,
+ ),
+ )
+ }
+
+ private fun test(
+ mediaItems: List,
+ expectedImageAndVideoItems: List,
+ expectedFileItems: List,
+ ) {
+ val sut = MediaItemsPostProcessor()
+ val result = sut.process(AsyncData.Success(mediaItems.toImmutableList()))
+ val data = result.dataOrNull()!!
+
+ // Compare the lists to have better failure info
+ assertThat(data.imageAndVideoItems.toList()).isEqualTo(expectedImageAndVideoItems)
+ assertThat(data.fileItems.toList()).isEqualTo(expectedFileItems)
+
+ assertThat(result).isEqualTo(
+ AsyncData.Success(
+ GroupedMediaItems(
+ imageAndVideoItems = expectedImageAndVideoItems.toImmutableList(),
+ fileItems = expectedFileItems.toImmutableList(),
+ )
+ )
+ )
+ }
+}
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
index c341f6e7515..11d8426c09f 100644
--- 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
@@ -11,6 +11,7 @@ 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.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.media.FakeMediaFile
import io.element.android.libraries.mediaviewer.api.MediaInfo
@@ -26,10 +27,15 @@ class AndroidLocalMediaFactoryTest {
@Test
fun `test AndroidLocalMediaFactory`() {
val sut = createAndroidLocalMediaFactory()
- val result = sut.createFromMediaFile(aMediaFile(), anImageMediaInfo(
- senderName = A_USER_NAME,
- dateSent = "12:34",
- ))
+ val result = sut.createFromMediaFile(
+ mediaFile = aMediaFile(),
+ mediaInfo = anImageMediaInfo(
+ senderId = A_USER_ID,
+ senderName = A_USER_NAME,
+ dateSent = "12:34",
+ dateSentFull = "full",
+ )
+ )
assertThat(result.uri.toString()).endsWith("aPath")
assertThat(result.info).isEqualTo(
MediaInfo(
@@ -38,8 +44,12 @@ class AndroidLocalMediaFactoryTest {
mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB",
fileExtension = "jpg",
+ senderId = A_USER_ID,
senderName = A_USER_NAME,
- dateSent = "12:34"
+ senderAvatar = null,
+ dateSent = "12:34",
+ dateSentFull = "full",
+ waveform = null,
)
)
}
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt
new file mode 100644
index 00000000000..c07c53f8ae8
--- /dev/null
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/FakeMediaViewerNavigator.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.mediaviewer.impl.viewer
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeMediaViewerNavigator(
+ private val onViewInTimelineClickLambda: (EventId) -> Unit = { lambdaError() },
+ private val onItemDeletedLambda: () -> Unit = { lambdaError() },
+) : MediaViewerNavigator {
+ override fun onViewInTimelineClick(eventId: EventId) {
+ onViewInTimelineClickLambda(eventId)
+ }
+
+ override fun onItemDeleted() {
+ onItemDeletedLambda()
+ }
+}
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt
index cbe334216cd..1e5f6120cc1 100644
--- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt
@@ -16,20 +16,35 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
+import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
+import io.element.android.libraries.matrix.test.AN_EVENT_ID
+import io.element.android.libraries.matrix.test.A_SESSION_ID_2
+import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.matrix.test.media.aMediaSource
+import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
+import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
+import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
import io.element.android.tests.testutils.WarmUpRule
+import io.element.android.tests.testutils.lambda.lambdaRecorder
+import io.element.android.tests.testutils.lambda.value
+import io.element.android.tests.testutils.test
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
-private val TESTED_MEDIA_INFO = anApkMediaInfo()
+private val TESTED_MEDIA_INFO = anApkMediaInfo(
+ senderId = A_USER_ID,
+)
class MediaViewerPresenterTest {
@get:Rule
@@ -38,11 +53,85 @@ class MediaViewerPresenterTest {
private val mockMediaUri: Uri = mockk("localMediaUri")
private val localMediaFactory = FakeLocalMediaFactory(mockMediaUri)
+ @Test
+ fun `present - initial state null Event`() = runTest {
+ val presenter = createMediaViewerPresenter(
+ room = FakeMatrixRoom(
+ canRedactOwnResult = { Result.success(true) },
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
+ assertThat(initialState.snackbarMessage).isNull()
+ assertThat(initialState.canShowInfo).isTrue()
+ assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ }
+ }
+
+ @Test
+ fun `present - initial state cannot show info`() = runTest {
+ val presenter = createMediaViewerPresenter(
+ canShowInfo = false,
+ room = FakeMatrixRoom(
+ canRedactOwnResult = { Result.success(true) },
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
+ assertThat(initialState.snackbarMessage).isNull()
+ assertThat(initialState.canShowInfo).isFalse()
+ assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ }
+ }
+
+ @Test
+ fun `present - initial state Event`() = runTest {
+ val presenter = createMediaViewerPresenter(
+ eventId = AN_EVENT_ID,
+ room = FakeMatrixRoom(
+ canRedactOwnResult = { Result.success(true) },
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
+ assertThat(initialState.snackbarMessage).isNull()
+ assertThat(initialState.canShowInfo).isTrue()
+ assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ }
+ }
+
+ @Test
+ fun `present - initial state Event from other`() = runTest {
+ val presenter = createMediaViewerPresenter(
+ eventId = AN_EVENT_ID,
+ room = FakeMatrixRoom(
+ sessionId = A_SESSION_ID_2,
+ canRedactOtherResult = { Result.success(false) },
+ )
+ )
+ presenter.test {
+ skipItems(2)
+ val initialState = awaitItem()
+ assertThat(initialState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
+ assertThat(initialState.snackbarMessage).isNull()
+ assertThat(initialState.canShowInfo).isTrue()
+ assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
+ }
+ }
+
@Test
fun `present - download media success scenario`() = runTest {
- val matrixMediaLoader = FakeMatrixMediaLoader()
- val mediaActions = FakeLocalMediaActions()
- val presenter = createMediaViewerPresenter(matrixMediaLoader, mediaActions)
+ val presenter = createMediaViewerPresenter(
+ room = FakeMatrixRoom(
+ canRedactOwnResult = { Result.success(true) },
+ )
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -60,10 +149,15 @@ class MediaViewerPresenterTest {
@Test
fun `present - check all actions`() = runTest {
- val matrixMediaLoader = FakeMatrixMediaLoader()
val mediaActions = FakeLocalMediaActions()
val snackbarDispatcher = SnackbarDispatcher()
- val presenter = createMediaViewerPresenter(matrixMediaLoader, mediaActions, snackbarDispatcher)
+ val presenter = createMediaViewerPresenter(
+ localMediaActions = mediaActions,
+ snackbarDispatcher = snackbarDispatcher,
+ room = FakeMatrixRoom(
+ canRedactOwnResult = { Result.success(true) },
+ )
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -108,8 +202,12 @@ class MediaViewerPresenterTest {
@Test
fun `present - download media failure then retry with success scenario`() = runTest {
val matrixMediaLoader = FakeMatrixMediaLoader()
- val mediaActions = FakeLocalMediaActions()
- val presenter = createMediaViewerPresenter(matrixMediaLoader, mediaActions)
+ val presenter = createMediaViewerPresenter(
+ matrixMediaLoader = matrixMediaLoader,
+ room = FakeMatrixRoom(
+ canRedactOwnResult = { Result.success(true) },
+ )
+ )
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -134,25 +232,95 @@ class MediaViewerPresenterTest {
}
}
+ @Test
+ fun `present - delete media success scenario`() = runTest {
+ val redactEventLambda = lambdaRecorder> { _, _ ->
+ Result.success(Unit)
+ }
+ val timeline = FakeTimeline().apply {
+ this.redactEventLambda = redactEventLambda
+ }
+ val onItemDeletedLambda = lambdaRecorder { }
+ val navigator = FakeMediaViewerNavigator(
+ onItemDeletedLambda = onItemDeletedLambda,
+ )
+
+ val presenter = createMediaViewerPresenter(
+ room = FakeMatrixRoom(
+ liveTimeline = timeline,
+ canRedactOwnResult = { Result.success(true) },
+ ),
+ mediaViewerNavigator = navigator,
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized)
+ assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO)
+ val loadingState = awaitItem()
+ assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
+ val successState = awaitItem()
+ assertThat(successState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
+ successState.eventSink(MediaViewerEvents.Delete(AN_EVENT_ID))
+ redactEventLambda.assertions()
+ .isCalledOnce()
+ .with(
+ value(AN_EVENT_ID.toEventOrTransactionId()),
+ value(null),
+ )
+ onItemDeletedLambda.assertions().isCalledOnce()
+ }
+ }
+
+ @Test
+ fun `present - view in timeline invokes the navigator`() = runTest {
+ val onViewInTimelineClickLambda = lambdaRecorder { }
+ val navigator = FakeMediaViewerNavigator(
+ onViewInTimelineClickLambda = onViewInTimelineClickLambda,
+ )
+ val presenter = createMediaViewerPresenter(
+ mediaViewerNavigator = navigator,
+ room = FakeMatrixRoom(
+ canRedactOwnResult = { Result.success(true) },
+ )
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ assertThat(initialState.downloadedMedia).isEqualTo(AsyncData.Uninitialized)
+ assertThat(initialState.mediaInfo).isEqualTo(TESTED_MEDIA_INFO)
+ val loadingState = awaitItem()
+ assertThat(loadingState.downloadedMedia).isInstanceOf(AsyncData.Loading::class.java)
+ val successState = awaitItem()
+ assertThat(successState.downloadedMedia).isInstanceOf(AsyncData.Success::class.java)
+ successState.eventSink(MediaViewerEvents.ViewInTimeline(AN_EVENT_ID))
+ onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
+ }
+ }
+
private fun createMediaViewerPresenter(
- matrixMediaLoader: FakeMatrixMediaLoader,
- localMediaActions: FakeLocalMediaActions,
+ eventId: EventId? = null,
+ matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
+ localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
- canShare: Boolean = true,
- canDownload: Boolean = true,
+ canShowInfo: Boolean = true,
+ mediaViewerNavigator: MediaViewerNavigator = FakeMediaViewerNavigator(),
+ room: MatrixRoom = FakeMatrixRoom(
+ liveTimeline = FakeTimeline(),
+ ),
): MediaViewerPresenter {
return MediaViewerPresenter(
inputs = MediaViewerEntryPoint.Params(
+ eventId = eventId,
mediaInfo = TESTED_MEDIA_INFO,
mediaSource = aMediaSource(),
thumbnailSource = null,
- canShare = canShare,
- canDownload = canDownload,
+ canShowInfo = canShowInfo,
),
localMediaFactory = localMediaFactory,
mediaLoader = matrixMediaLoader,
localMediaActions = localMediaActions,
snackbarDispatcher = snackbarDispatcher,
+ navigator = mediaViewerNavigator,
+ room = room,
)
}
}
diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt
index acbfb576190..83fada0e546 100644
--- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt
+++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt
@@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
+import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
@@ -54,17 +55,33 @@ class MediaViewerViewTest {
testMenuAction(CommonStrings.action_open_with, MediaViewerEvents.OpenWith)
}
+ private fun testMenuAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
+ val eventsRecorder = EventsRecorder()
+ rule.setMediaViewerView(
+ aMediaViewerState(
+ downloadedMedia = AsyncData.Success(
+ LocalMedia(Uri.EMPTY, anImageMediaInfo())
+ ),
+ mediaInfo = anImageMediaInfo(),
+ eventSink = eventsRecorder
+ ),
+ )
+ val contentDescription = rule.activity.getString(contentDescriptionRes)
+ rule.onNodeWithContentDescription(contentDescription).performClick()
+ eventsRecorder.assertSingle(expectedEvent)
+ }
+
@Test
fun `clicking on save emit expected Event`() {
- testMenuAction(CommonStrings.action_save, MediaViewerEvents.SaveOnDisk)
+ testBottomSheetAction(CommonStrings.action_save, MediaViewerEvents.SaveOnDisk)
}
@Test
fun `clicking on share emit expected Event`() {
- testMenuAction(CommonStrings.action_share, MediaViewerEvents.Share)
+ testBottomSheetAction(CommonStrings.action_share, MediaViewerEvents.Share)
}
- private fun testMenuAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
+ private fun testBottomSheetAction(contentDescriptionRes: Int, expectedEvent: MediaViewerEvents) {
val eventsRecorder = EventsRecorder()
rule.setMediaViewerView(
aMediaViewerState(
@@ -72,11 +89,11 @@ class MediaViewerViewTest {
LocalMedia(Uri.EMPTY, anImageMediaInfo())
),
mediaInfo = anImageMediaInfo(),
+ mediaBottomSheetState = aMediaDetailsBottomSheetState(),
eventSink = eventsRecorder
),
)
- val contentDescription = rule.activity.getString(contentDescriptionRes)
- rule.onNodeWithContentDescription(contentDescription).performClick()
+ rule.clickOn(contentDescriptionRes)
eventsRecorder.assertSingle(expectedEvent)
}
diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt
index a0f36c6f0fb..f5f28007d4f 100644
--- a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt
+++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt
@@ -37,8 +37,12 @@ class FakeLocalMediaFactory(
mimeType = mimeType ?: fallbackMimeType,
formattedFileSize = formattedFileSize ?: fallbackFileSize,
fileExtension = fileExtensionExtractor.extractFromName(safeName),
+ senderId = null,
senderName = null,
- dateSent = null
+ senderAvatar = null,
+ dateSent = null,
+ dateSentFull = null,
+ waveform = null,
)
return aLocalMedia(uri, mediaInfo)
}
diff --git a/libraries/push/impl/src/main/res/values-fr/translations.xml b/libraries/push/impl/src/main/res/values-fr/translations.xml
index eceefa879cc..eb386ed459f 100644
--- a/libraries/push/impl/src/main/res/values-fr/translations.xml
+++ b/libraries/push/impl/src/main/res/values-fr/translations.xml
@@ -24,7 +24,7 @@
"Vous a invité(e) à discuter"
"%1$s vous a invité à discuter"
- "Mentionné(e): %1$s"
+ "Mentionné(e) : %1$s"
"Nouveaux messages"
- "%d nouveau message"
@@ -68,7 +68,7 @@
"Vérifier que l’application peut afficher des notifications."
"Vous n’avez pas cliqué sur la notification."
"Impossible d’afficher la notification."
- "Vous avez cliqué sur la notification!"
+ "Vous avez cliqué sur la notification !"
"Affichage des notifications"
"Veuillez cliquer sur la notification pour continuer le test."
"Vérifier que l’application reçoit les Push."
diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt
index b7b0368aeb0..192527ab890 100644
--- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt
+++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt
@@ -18,10 +18,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.LinearGradientShader
-import androidx.compose.ui.graphics.RadialGradientShader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -39,6 +37,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
/**
* Send button for the message composer.
* Figma: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1956-37575&node-type=frame&m=dev
+ * Temporary Figma : https://www.figma.com/design/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?node-id=2274-39944&m=dev
*/
@Composable
internal fun SendButton(
@@ -105,18 +104,10 @@ private fun buttonBackgroundModifier() = Modifier.drawWithCache {
from = Offset(0f, 0f),
to = Offset(0f, height),
colors = listOf(
- Color(0xFF0BC491),
- Color(0xFF0467DD),
- )
- )
- )
- val radialGradientBrush = ShaderBrush(
- RadialGradientShader(
- center = Offset(height / 2f, height / 2f),
- radius = height / 2f,
- colors = listOf(
- Color(0xFF0BC491),
- Color(0xFF0467DD),
+ Color(0xFF79DD98),
+ Color(0xFF0DBD8B),
+ Color(0xFF128585),
+ Color(0xFF24446B),
)
)
)
@@ -124,11 +115,6 @@ private fun buttonBackgroundModifier() = Modifier.drawWithCache {
drawRect(
brush = verticalGradientBrush,
)
- drawRect(
- brush = radialGradientBrush,
- alpha = 0.4f,
- blendMode = BlendMode.Overlay,
- )
}
}
diff --git a/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml b/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml
index 4e5661dce0a..a7d29139377 100644
--- a/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml
+++ b/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml
@@ -4,7 +4,7 @@
"Numeroimaton luettelo päälle/pois"
"Sulje muotoiluasetukset"
"Koodilohko päälle/pois"
- "Valinnainen kuvateksti…"
+ "Lisää kuvateksti"
"Viesti…"
"Luo linkki"
"Muokkaa linkkiä"
diff --git a/libraries/textcomposer/impl/src/main/res/values-it/translations.xml b/libraries/textcomposer/impl/src/main/res/values-it/translations.xml
index 4db0f2cb957..04e45678ebe 100644
--- a/libraries/textcomposer/impl/src/main/res/values-it/translations.xml
+++ b/libraries/textcomposer/impl/src/main/res/values-it/translations.xml
@@ -4,6 +4,7 @@
"Attiva/disattiva l\'elenco puntato"
"Chiudi le opzioni di formattazione"
"Attiva/disattiva il blocco di codice"
+ "Aggiungi una didascalia"
"Messaggio…"
"Crea un collegamento"
"Modifica collegamento"
diff --git a/libraries/ui-strings/src/main/res/values-be/translations.xml b/libraries/ui-strings/src/main/res/values-be/translations.xml
index 1e98029dda1..fbea21c5126 100644
--- a/libraries/ui-strings/src/main/res/values-be/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-be/translations.xml
@@ -296,7 +296,6 @@
"Замацаваныя паведамленні"
"Адклікаць праверку і адправіць"
"Усё роўна адправіць паведамленне"
- "Замацаваныя паведамленні"
"Не атрымалася апрацаваць медыяфайл для загрузкі, паспрабуйце яшчэ раз."
"Не ўдалося атрымаць інфармацыю пра карыстальніка"
"%1$s з %2$s"
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 652031d2c23..babe41a1423 100644
--- a/libraries/ui-strings/src/main/res/values-cs/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml
@@ -48,8 +48,10 @@
"Potvrdit heslo"
"Pokračovat"
"Kopírovat"
+ "Kopírovat titulek"
"Kopírovat odkaz"
"Kopírovat odkaz na zprávu"
+ "Kopírovat text"
"Vytvořit"
"Vytvořit místnost"
"Deaktivovat"
@@ -96,6 +98,7 @@
"Odmítnout"
"Odstranit"
"Odstranit titulek"
+ "Odstranit zprávu"
"Odpovědět"
"Odpovědět ve vlákně"
"Nahlásit chybu"
@@ -126,6 +129,7 @@
"Zobrazit na časové ose"
"Zobrazit zdroj"
"Ano"
+ "Ano, zkusit znovu"
"O aplikaci"
"Zásady používání"
"Přidání titulku"
@@ -147,6 +151,7 @@
"ID zařízení"
"Přímý chat"
"Znovu nezobrazovat"
+ "Stahování"
"(upraveno)"
"Úpravy"
"Úprava titulku"
@@ -300,12 +305,8 @@ Důvod: %1$s."
"Ahoj, ozvi se mi na %1$s: %2$s"
"%1$s Android"
"Zatřeste zařízením pro nahlášení chyby"
- "Přijmout vše"
- "Odmítnout a vykázat"
- "Když někdo požádá o vstup do místnosti, uvidíte jeho žádost zde."
- "Žádná čekající žádost o vstup"
- "Žádosti o vstup"
"Výběr média se nezdařil, zkuste to prosím znovu."
+ "Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Přidržte zprávu a vyberte „%1$s“, kterou chcete zahrnout sem."
@@ -326,25 +327,30 @@ Důvod: %1$s."
"Vaše zpráva nebyla odeslána, protože%1$s neověřil(a) všechna zařízení"
"Jedno nebo více vašich zařízení není ověřeno. Zprávu můžete přesto odeslat, nebo ji můžete prozatím zrušit a zkusit to znovu později, až ověříte všechna svá zařízení."
"Vaše zpráva nebyla odeslána, protože jste neověřili jedno nebo více zařízení"
- "Připnuté zprávy"
- "Žádosti o vstup"
"Nahrání média se nezdařilo, zkuste to prosím znovu."
"Nepodařilo se načíst údaje o uživateli"
-
- - "%1$s +%2$d další chce vstoupit do této místnosti"
- - "%1$s +%2$d další chtějí vstoupit do této místnosti"
- - "%1$s +%2$d dalších chce vstoupit do této místnosti"
-
- "Zobrazit vše"
"%1$s z %2$s"
"%1$s Připnuté zprávy"
"Načítání zprávy…"
"Zobrazit vše"
- "Přijmout"
- "%1$s chce vstoupit do této místnosti"
- "Zobrazit"
"Chat"
"Žádost o vstup odeslána"
+ "Kdokoli může požádat o vstup do místnosti, ale správce nebo moderátor bude muset žádost přijmout."
+ "Požádat o vstup"
+ "Ano, povolit šifrování"
+ "Po aktivaci nelze šifrování místnosti deaktivovat. Historie zpráv bude viditelná pouze pro členy místnosti od doby, kdy byli pozváni nebo od té doby, co do místnosti vstoupili.
+Nikdo kromě členů místnosti nebude moci číst zprávy. To může bránit správnému fungování robotů a propojení.
+Nedoporučujeme povolovat šifrování pro místnosti, které může kdokoli najít a vstoupit do nich."
+ "Povolit šifrování?"
+ "Jakmile je povoleno, šifrování nelze zakázat."
+ "Šifrování"
+ "Povolit koncové šifrování"
+ "Každý může najít a vstoupit"
+ "Kdokoliv"
+ "Lidé mohou vstoupit, pouze pokud jsou pozváni"
+ "Pouze pro zvané"
+ "Přístup do místnosti"
+ "Zabezpečení a soukromí"
"Sdílet polohu"
"Sdílet moji polohu"
"Otevřít v Mapách Apple"
@@ -357,4 +363,8 @@ Důvod: %1$s."
"Poloha"
"Verze: %1$s (%2$s)"
"en"
+ "Historické zprávy nejsou na tomto zařízení k dispozici"
+ "Nemáte přístup k této zprávě"
+ "Nelze dešifrovat zprávu"
+ "Tato zpráva byla zablokována buď proto, že jste neověřili své zařízení, nebo proto, že odesílatel potřebuje ověřit vaši totožnost."
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 826730030de..0d7acd7e900 100644
--- a/libraries/ui-strings/src/main/res/values-de/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-de/translations.xml
@@ -299,20 +299,6 @@ Grund: %1$s."
"Hey, sprich mit mir auf %1$s: %2$s"
"%1$s Android"
"Schüttel heftig zum Melden von Fehlern"
- "Ja, akzeptiere alle"
- "Sind Sie sicher, dass Sie alle Beitrittsanfragen akzeptieren möchten?"
- "Akzeptiere alle Anfragen"
- "Alle akzeptieren"
- "Ja, ablehnen und sperren"
- "Sind Sie sicher, dass Sie %1$s ablehnen und sperren möchten ? Dieser Benutzer kann keinen erneuten Zugriff auf diesen Raum anfordern."
- "Ablehnen und Zugriff verbieten"
- "Ja, ablehnen"
- "Sind Sie sicher, dass Sie die %1$s Anfrage, diesem Chatroom beizutreten, ablehnen möchten ?"
- "Zugriff verweigern"
- "Ablehnen und sperren"
- "Falls jemand um Aufnahme in den Raum bittet, können Sie dessen Anfrage hier sehen."
- "Keine ausstehende Beitrittsanfrage"
- "Beitrittsanfragen"
"Medienauswahl fehlgeschlagen, bitte versuche es erneut."
"Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar."
"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."
@@ -334,22 +320,12 @@ Grund: %1$s."
"Deine Nachricht wurde nicht gesendet, weil %1$s nicht alle Geräte verifiziert hat"
"Mindestens eines Ihrer Geräte ist nicht verifiziert worden. Sie können die Nachricht trotzdem senden, oder den Vorgang zunächst abbrechen und es später erneut versuchen, nachdem Sie alle Ihrer Geräte verifiziert haben."
"Ihre Nachricht wurde nicht geschickt, da Sie eines oder mehrere Ihrer Geräte nicht verifiziert haben."
- "Fixierte Nachrichten"
- "Beitrittsanfragen"
"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."
"Benutzerdetails konnten nicht abgerufen werden"
-
- - "%1$s+ %2$d andere wollen diesem Chatroom beitreten"
- - "%1$s+ %2$d andere wollen diesem Chatroom beitreten"
-
- "Alles ansehen"
"%1$s von %2$s"
"%1$s fixierte Nachrichten"
"Nachricht wird geladen…"
"Alle anzeigen"
- "Akzeptieren"
- "%1$s möchte diesem Chatroom beitreten"
- "Ansicht"
"Chat"
"Beitrittsanfrage gesendet"
"Standort teilen"
@@ -364,4 +340,7 @@ Grund: %1$s."
"Standort"
"Version: %1$s (%2$s)"
"en"
+ "Der Nachrichtenverlauf ist auf diesem Gerät nicht verfügbar"
+ "Nachricht kann nicht entschlüsselt werden"
+ "Diese Nachricht wurde entweder blockiert, weil Ihr Gerät nicht verifiziert ist oder weil der Absender Ihre Identität überprüfen muss."
diff --git a/libraries/ui-strings/src/main/res/values-el/translations.xml b/libraries/ui-strings/src/main/res/values-el/translations.xml
index 39b8bc5b9aa..5ee3c84a84f 100644
--- a/libraries/ui-strings/src/main/res/values-el/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-el/translations.xml
@@ -148,6 +148,7 @@
"ID συσκευής"
"Άμεση συνομιλία"
"Να μην εμφανιστεί ξανά"
+ "Γίνεται λήψη"
"(επεξεργάστηκε)"
"Επεξεργάζεται"
"Η λεζάντα επεξεργάζεται"
@@ -299,20 +300,6 @@
"Γεια, μίλα μου στην εφαρμογή %1$s :%2$s"
"%1$s Android"
"Κούνησε δυνατά τη συσκευή σου για να αναφέρεις κάποιο σφάλμα"
- "Ναι, αποδοχή όλων"
- "Σίγουρα θες να αποδεχτείς όλα τα αιτήματα συμμετοχής;"
- "Αποδοχή όλων των αιτημάτων"
- "Αποδοχή όλων"
- "Ναι, απόρριψη και αποκλεισμός"
- "Σίγουρα θες να απορρίψειε και να αποκλείσεις τον χρήστη %1$s; Αυτός ο χρήστης δεν θα μπορεί να ζητήσει πρόσβαση για να συμμετάσχει ξανά σε αυτό το δωμάτιο."
- "Απόρριψη και αποκλεισμός πρόσβασης"
- "Ναι, απόρριψη"
- "Σίγουρα θες να απορρίψεις το αίτημα του χρήστη %1$s να συμμετάσχει στο δωμάτιο;"
- "Απόρριψη πρόσβασης"
- "Απόρριψη και αποκλεισμός"
- "Όταν κάποιος θα ζητήσει να συμμετάσχει στο δωμάτιο, θα μπορείς να δεις το αίτημά του εδώ."
- "Δεν υπάρχει εκκρεμές αίτημα συμμετοχής"
- "Αιτήματα συμμετοχής"
"Αποτυχία επιλογής πολυμέσου, δοκίμασε ξανά."
"Οι λεζάντες ενδέχεται να μην είναι ορατές σε άτομα που χρησιμοποιούν παλαιότερες εφαρμογές."
"Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."
@@ -334,24 +321,30 @@
"Το μήνυμά σου δεν στάλθηκε επειδή ο χρήστης %1$s δεν έχει επαληθεύσει όλες τις συσκευές"
"Μία ή περισσότερες από τις συσκευές σου δεν έχουν επαληθευτεί. Μπορείς να στείλεις το μήνυμα ούτως ή άλλως, ή μπορείς να το ακυρώσεις προς το παρόν και να προσπαθήσεις ξανά αργότερα αφού επαληθεύσεις όλες τις συσκευές σου."
"Το μήνυμά σου δεν στάλθηκε επειδή δεν έχεις επαληθεύσει τουλάχιστον μία από τις συσκευές σου"
- "Καρφιτσωμένα μηνύματα"
- "Αιτήματα συμμετοχής"
"Αποτυχία μεταφόρτωσης μέσου, δοκίμασε ξανά."
"Δεν ήταν δυνατή η ανάκτηση στοιχείων χρήστη"
-
- - "Οι χρήστες %1$s +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο"
- - "Οι χρήστες %1$s +%2$d ακόμη θέλουν να συμμετάσχουν σε αυτό το δωμάτιο"
-
- "Προβολή όλων"
"%1$s από %2$s"
"%1$s Καρφιτσωμένα μηνύματα"
"Φόρτωση μηνύματος…"
"Προβολή Όλων"
- "Αποδοχή"
- "Ο χρήστης %1$s θέλει να μπει σε αυτό το δωμάτιο"
- "Προβολή"
"Συνομιλία"
"Το αίτημα συμμετοχής στάλθηκε"
+ "Οποιοσδήποτε μπορεί να ζητήσει να συμμετάσχει στο δωμάτιο, αλλά κάποιος διαχειριστής ή συντονιστής θα πρέπει να αποδεχθεί το αίτημα."
+ "Αίτημα συμμετοχής"
+ "Ναι, ενεργοποιήστε την κρυπτογράφηση"
+ "Μόλις ενεργοποιηθεί, η κρυπτογράφηση για ένα δωμάτιο δεν μπορεί να απενεργοποιηθεί. Το ιστορικό μηνυμάτων θα είναι ορατό μόνο για τα μέλη του δωματίου από τότε που προσκλήθηκαν ή από τότε που εντάχθηκαν στην αίθουσα.
+Κανείς εκτός από τα μέλη του δωματίου δεν θα μπορεί να διαβάσει μηνύματα. Αυτό μπορεί να αποτρέψει τη σωστή λειτουργία των bots και των γεφυρών.
+Δεν συνιστούμε να ενεργοποιήσεις την κρυπτογράφηση για δωμάτια στα οποία μπορεί κανείς να βρει και να συμμετάσχει."
+ "Ενεργοποίηση κρυπτογράφησης;"
+ "Μόλις ενεργοποιηθεί, η κρυπτογράφηση δεν μπορεί να απενεργοποιηθεί."
+ "Κρυπτογράφηση"
+ "Ενεργοποίηση κρυπτογράφησης από άκρο σε άκρο"
+ "Οποιοσδήποτε μπορεί να βρει και να συμμετάσχει"
+ "Οποιοσδήποτε"
+ "Τα άτομα μπορούν να συμμετάσχουν μόνο εάν έχουν προσκληθεί"
+ "Μόνο πρόσκληση"
+ "Πρόσβαση δωματίου"
+ "Ασφάλεια & απόρρητο"
"Κοινή χρήση τοποθεσίας"
"Κοινή χρήση της τοποθεσίας μου"
"Άνοιγμα στο Apple Maps"
@@ -364,4 +357,8 @@
"Τοποθεσία"
"Έκδοση: %1$s (%2$s)"
"el"
+ "Τα ιστορικά μηνύματα δεν είναι διαθέσιμα σε αυτήν τη συσκευή"
+ "Δεν έχεις πρόσβαση σε αυτό το μήνυμα"
+ "Δεν είναι δυνατή η αποκρυπτογράφηση μηνύματος"
+ "Αυτό το μήνυμα αποκλείστηκε είτε επειδή δεν επαλήθευσες τη συσκευή σου είτε επειδή ο αποστολέας πρέπει να επαληθεύσει την ταυτότητά σου."
diff --git a/libraries/ui-strings/src/main/res/values-et/translations.xml b/libraries/ui-strings/src/main/res/values-et/translations.xml
index 3871a58914c..7baf58a5be9 100644
--- a/libraries/ui-strings/src/main/res/values-et/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-et/translations.xml
@@ -148,6 +148,7 @@
"Seadme tunnus"
"Otsevestlus"
"Ära enam näita seda uuesti"
+ "Laadime alla"
"(muudetud)"
"Muutmine"
"Muudame selgitust"
@@ -299,20 +300,6 @@ Põhjus: %1$s."
"Hei, suhtle minuga %1$s võrgus: %2$s"
"%1$s Android"
"Veast teatamiseks raputa nutiseadet ägedalt"
- "Jah, võta kõik vastu"
- "Kas sa oled kindel, et soovid kõik vastu liitumist soovinud võtta?"
- "Võta kõik vastu"
- "Nõustu kõigiga"
- "Jah, keeldu liitumisest ning keela ligipääs"
- "Kas sa oled kindel, et soovid kasutajale %1$s keelata ligipääsu siia jututuppa ning seada talle suhtluskeelu? Seetõttu ta ei saa ka enam hiljem liitumispalvet saata."
- "Keeldu liitumisest ja keela ligipääs"
- "Jah, keeldu"
- "Kas sa oled kindel, et soovid kasutajale %1$s keelata ligipääsu siia jututuppa?"
- "Keela ligipääs"
- "Keeldu ja määra suhtluskeeld"
- "Kui keegi soovib jututoaga liituda, siis need päringud on kuvatud siin."
- "Pole ühtegi liitumispalvet"
- "Liitumispalved"
"Meediafaili valimine ei õnnestunud. Palun proovi uuesti."
"Selgitused ja alapealkirjad ei pruugi olla nähtavad vanemate rakenduste kasutajatele."
"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."
@@ -334,22 +321,12 @@ Põhjus: %1$s."
"Sinu sõnum on saatmata, kuna %1$s pole verifitseerinud kõiki oma seadmeid"
"Üks või enam sinu seadet on verifitseerimata. Sa võid sõnumi ikkagi ära saata või katkestad saatmise ning proovid uuesti, kui oled kõik oma seadmed verifitseerinud."
"Kuna sul on üks või enam verifitseerimata seadet, siis sinu sõnum jäi saatmata"
- "Esiletõstetud sõnumid"
- "Liitumispalved"
"Meediafaili töötlemine enne üleslaadimist ei õnnestunud. Palun proovi uuesti."
"Kasutaja andmete laadimine ei õnnestunud"
-
- - "%1$s + veel %2$d kasutaja soovivad selle jututoaga liituda"
- - "%1$s + veel %2$d kasutajat soovivad selle jututoaga liituda"
-
- "Vaata kõiki"
"%1$s / %2$s"
"%1$s esiletõstetud sõnumit"
"Laadime sõnumit…"
"Näita kõiki"
- "Nõustu"
- "%1$s soovib selle jututoaga liituda"
- "Vaata"
"Vestlus"
"Liitumispäring on saadetud"
"Jaga asukohta"
@@ -364,4 +341,8 @@ Põhjus: %1$s."
"Asukoht"
"Versioon: %1$s (%2$s)"
"et"
+ "Vanu sõnumeid ei saa selles seadmes näha"
+ "Sul puudub ligipääs sellele sõnumile"
+ "Sõnumi dekrüptimine ei õnnestu"
+ "Kuna seade on verifitseerimata või saatja pole sind verifitseerinud, siis sõnumi näitamine on blokeeritud."
diff --git a/libraries/ui-strings/src/main/res/values-fa/translations.xml b/libraries/ui-strings/src/main/res/values-fa/translations.xml
index cadcd7c6fa2..b41246e3b77 100644
--- a/libraries/ui-strings/src/main/res/values-fa/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-fa/translations.xml
@@ -263,7 +263,6 @@
"پیامهای سنجاق شده"
"داردید برای بازنشانی هویتتان به حساب %1$s میروید. پس از آن به کاره برگردانده خواهید شد."
"فرستادن پیام به هر روی"
- "پیامهای سنجاق شده"
"پردازش رسانه برای بارگذاری شکست خورد. لطفاً دوباره تلاش کنید."
"%1$s از %2$s"
"%1$s پیامهای سنجاق شده"
diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml
index bb6696ebfa8..34643e42d4b 100644
--- a/libraries/ui-strings/src/main/res/values-fi/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml
@@ -32,6 +32,7 @@
"Nauhoita ääniviesti."
"Lopeta nauhoittaminen"
"Hyväksy"
+ "Lisää kuvateksti"
"Lisää aikajanalle"
"Takaisin"
"Soita"
@@ -45,8 +46,10 @@
"Vahvista salasana"
"Jatka"
"Kopioi"
+ "Kopioi kuvateksti"
"Kopioi linkki"
"Kopioi linkki viestiin"
+ "Kopioi teksti"
"Luo"
"Luo huone"
"Deaktivoi"
@@ -57,6 +60,7 @@
"Hylkää"
"Valmis"
"Muokkaa"
+ "Muokkaa kuvatekstiä"
"Muokkaa kyselyä"
"Ota käyttöön"
"Lopeta kysely"
@@ -91,6 +95,8 @@
"Reagoi"
"Hylkää"
"Poista"
+ "Poista kuvateksti"
+ "Poista viesti"
"Vastaa"
"Vastaa ketjuun"
"Ilmoita virheestä"
@@ -123,6 +129,7 @@
"Kyllä"
"Tietoa"
"Hyväksyttävän käytön käytäntö"
+ "Lisätään kuvatekstiä"
"Edistyneet asetukset"
"Analytiikka"
"Ulkoasu"
@@ -143,6 +150,7 @@
"Älä näytä tätä uudelleen"
"(muokattu)"
"Muokataan viestiä"
+ "Muokataan kuvatekstiä"
"* %1$s %2$s"
"Salaus"
"Salaus käytössä"
@@ -292,6 +300,7 @@ Syy: %1$s."
"%1$s Android"
"Raivostunut ravistaminen ilmoittaa virheestä"
"Median valinta epäonnistui, yritä uudelleen."
+ "Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia."
"Median käsittely epäonnistui, yritä uudelleen."
"Median lähettäminen epäonnistui, yritä uudelleen."
"Paina viestiä ja valitse “%1$s” lisätäksesi sen tänne."
@@ -311,7 +320,6 @@ Syy: %1$s."
"Viestiäsi ei lähetetty, koska %1$s ei ole vahvistanut kaikkia laitteitaan."
"Yksi tai useampi laitteistasi on vahvistamaton. Voit lähettää viestin silti tai peruuttaa sen toistaiseksi ja yrittää uudelleen myöhemmin, kun olet vahvistanut kaikki laitteesi."
"Viestiäsi ei lähetetty, koska et ole vahvistanut yhtä tai useampaa laitettasi."
- "Kiinnitetyt viestit"
"Median käsittely epäonnistui, yritä uudelleen."
"Käyttäjän tietojen hakeminen epäonnistui"
"%1$s / %2$s"
@@ -332,4 +340,7 @@ Syy: %1$s."
"Sijainti"
"Versio: %1$s (%2$s)"
"fi"
+ "Viestihistoria ei ole saatavilla tällä laitteella"
+ "Viestin salauksen purkaminen ei onnistu"
+ "Tämä viesti estettiin, koska laitettasi ei ole vahvistettu tai koska lähettäjän on vahvistettava identiteettisi."
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 24bd75cf463..3d75daa0291 100644
--- a/libraries/ui-strings/src/main/res/values-fr/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml
@@ -127,6 +127,7 @@
"Voir dans la discussion"
"Afficher la source"
"Oui"
+ "Oui, réessayez"
"À propos"
"Politique d’utilisation acceptable"
"Ajout d’une légende"
@@ -148,6 +149,7 @@
"Identifiant de session"
"Discussion à deux"
"Ne plus afficher"
+ "En cours de téléchargement"
"(modifié)"
"Édition"
"Modification de la légende"
@@ -158,7 +160,7 @@
"Erreur"
"Une erreur s’est produite, il est possible que vous ne receviez pas de notifications pour les nouveaux messages. Veuillez résoudre les problèmes liés aux notifications depuis les paramètres.
-Raison: %1$s."
+Raison : %1$s."
"Tout le monde"
"Échec"
"Favori"
@@ -275,8 +277,8 @@ Raison: %1$s."
"Erreur"
"Succès"
"Attention"
- "Vos modifications n’ont pas été enregistrées. Êtes-vous certain de vouloir quitter?"
- "Enregistrer les changements?"
+ "Vos modifications n’ont pas été enregistrées. Êtes-vous certain de vouloir quitter ?"
+ "Enregistrer les changements ?"
"Votre serveur d’accueil doit être mis à jour pour prendre en charge le protocole MAS (Matrix Authentication Service) et la création de compte."
"Échec de la création du permalien"
"%1$s n’a pas pu charger la carte. Veuillez réessayer ultérieurement."
@@ -299,20 +301,6 @@ Raison: %1$s."
"Salut, parle-moi sur %1$s : %2$s"
"%1$s Android"
"Rageshake pour signaler un problème"
- "Oui, tout accepter"
- "Êtes-vous sûr de vouloir accepter toutes les demandes pour rejoindre le salon ?"
- "Tout accepter"
- "Tout accepter"
- "Oui, rejeter et bannir"
- "Êtes-vous sûr de vouloir rejeter la demande et bannir %1$s? Cet utilisateur ne pourra pas demander à nouveau à rejoindre ce salon."
- "Refuser et interdire l’accès"
- "Oui, refuser"
- "Êtes-vous sûr de vouloir refuser la demande de %1$s à rejoindre le salon?"
- "Refuser l’accès"
- "Refuser et bannir"
- "Lorsque quelqu’un demandera à rejoindre le salon, vous pourrez voir sa demande ici."
- "Personne ne demande à rejoindre le salon"
- "Demandes en attente"
"Échec de la sélection du média, veuillez réessayer."
"Les légendes peuvent ne pas être visibles pour les utilisateurs d’anciennes applications."
"Échec du traitement des médias à télécharger, veuillez réessayer."
@@ -334,24 +322,30 @@ Raison: %1$s."
"Votre message n’a pas été envoyé car %1$s n’a pas vérifié tous ses appareils"
"Un ou plusieurs de vos appareils ne sont pas vérifiés. Vous pouvez quand même envoyer le message, ou vous pouvez annuler et réessayer plus tard après avoir vérifié tous vos appareils."
"Votre message n’a pas été envoyé car vous n’avez pas vérifié tous vos appareils"
- "Messages épinglés"
- "Demandes en attente"
"Échec du traitement des médias à télécharger, veuillez réessayer."
"Impossible de récupérer les détails de l’utilisateur"
-
- - "%1$s et %2$d autre personne souhaitent rejoindre ce salon"
- - "%1$s et %2$d autres personnes souhaitent rejoindre ce salon"
-
- "Tout afficher"
"%1$s sur %2$s"
"%1$s Messages épinglés"
"Chargement du message…"
"Voir tout"
- "Accepter"
- "%1$s souhaite rejoindre ce salon"
- "Voir"
"Discussion"
"Demande d’adhésion envoyée"
+ "N’importe qui peut demander à rejoindre le salon, mais un administrateur ou un modérateur devra accepter la demande."
+ "Demander à rejoindre"
+ "Oui, activer le chiffrement"
+ "Une fois activé, le chiffrement d’un salon ne peut pas être désactivé. L’historique des messages ne sera visible que pour les membres depuis qu’ils ont été invités ou depuis qu’ils ont rejoint le salon.
+Personne d’autre que les membres du salon ne pourra lire les messages. Cela peut empêcher les bots et les bridges de fonctionner correctement.
+Nous ne recommandons pas d’activer le chiffrement pour les salons que tout le monde peut trouver et rejoindre."
+ "Activer le chiffrement ?"
+ "Une fois activé, le chiffrement ne peut pas être désactivé."
+ "Chiffrement"
+ "Activer le chiffrement de bout en bout"
+ "Tout le monde peut le trouver et le rejoindre"
+ "Tout le monde"
+ "Le salon ne peut être joint que par les personnes invitées"
+ "Sur invitation uniquement"
+ "Accès au salon"
+ "Sécurité & confidentialité"
"Partage de position"
"Partager ma position"
"Ouvrir dans Apple Maps"
@@ -364,4 +358,8 @@ Raison: %1$s."
"Position"
"Version : %1$s ( %2$s )"
"fr"
+ "Les anciens messages ne sont pas disponibles sur cet appareil"
+ "Vous n’avez pas accès à ce message"
+ "Impossible de déchiffrer le message"
+ "Ce message a été bloqué soit parce que vous n’avez pas vérifié votre session, soit parce que l’expéditeur doit vérifier votre identité."
diff --git a/libraries/ui-strings/src/main/res/values-hu/translations.xml b/libraries/ui-strings/src/main/res/values-hu/translations.xml
index d6e41bc7482..4fe2418fa6b 100644
--- a/libraries/ui-strings/src/main/res/values-hu/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-hu/translations.xml
@@ -32,6 +32,7 @@
"Hangüzenet felvétele."
"Rögzítés leállítása"
"Elfogadás"
+ "Felirat hozzáadása"
"Hozzáadás az idővonalhoz"
"Vissza"
"Hívás"
@@ -45,8 +46,10 @@
"Jelszó megerősítése"
"Folytatás"
"Másolás"
+ "Felirat másolása"
"Hivatkozás másolása"
"Üzenetre mutató hivatkozás másolása"
+ "Szöveg másolása"
"Létrehozás"
"Szoba létrehozása"
"Deaktiválás"
@@ -57,6 +60,7 @@
"Elvetés"
"Kész"
"Szerkesztés"
+ "Felirat szerkesztése"
"Szavazás szerkesztése"
"Engedélyezés"
"Szavazás lezárása"
@@ -91,6 +95,8 @@
"Reakció"
"Elutasítás"
"Eltávolítás"
+ "Felirat eltávolítása"
+ "Üzenet eltávolítása"
"Válasz"
"Válasz az üzenetszálban"
"Hiba jelentése"
@@ -121,8 +127,10 @@
"Megtekintés az idővonalon"
"Forrás megtekintése"
"Igen"
+ "Igen, újrapróbálkozás"
"Névjegy"
"Elfogadható használatra vonatkozó szabályzat"
+ "Felirat hozzáadása"
"Speciális beállítások"
"Elemzések"
"Megjelenítés"
@@ -141,8 +149,10 @@
"Eszközazonosító"
"Közvetlen csevegés"
"Ne jelenjen meg többé"
+ "Letöltés"
"(szerkesztve)"
"Szerkesztés"
+ "Felirat szerkesztése"
"* %1$s %2$s"
"Titkosítás"
"Titkosítás engedélyezve"
@@ -246,6 +256,7 @@ Ok: %1$s."
"Nem sikerült elküldeni a meghívót (meghívókat)"
"Feloldás"
"Némítás feloldása"
+ "Nem támogatott hívás"
"Nem támogatott esemény"
"Felhasználónév"
"Az ellenőrzés megszakítva"
@@ -291,6 +302,7 @@ Ok: %1$s."
"%1$s Android"
"Az eszköz rázása a hibajelentéshez"
"Nem sikerült kiválasztani a médiát, próbálja újra."
+ "Előfordulhat, hogy a feliratok nem láthatók a régebbi alkalmazásokat használók számára."
"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."
"Nem sikerült a média feltöltése, próbálja újra."
"Nyomjon hosszan az üzenetre, és válassza a „%1$s” lehetőséget, hogy itt szerepeljen."
@@ -310,7 +322,6 @@ Ok: %1$s."
"Az üzenet nem lett elküldve, mert %1$s nem ellenőrizte az összes eszközét"
"Egy vagy több eszköze nincs ellenőrizve. Így is elküldheti az üzenetet, vagy egyelőre megszakíthatja, és később, az összes eszköz ellenőrzése után újrapróbálkozhat."
"Az üzenet nem lett elküldve, mert egy vagy több eszközét nem ellenőrizte"
- "Kitűzött üzenetek"
"Nem sikerült feldolgozni a feltöltendő médiát, próbálja újra."
"Nem sikerült letölteni a felhasználói adatokat"
"%1$s / %2$s"
@@ -319,6 +330,22 @@ Ok: %1$s."
"Összes megtekintése"
"Csevegés"
"Csatlakozási kérés elküldve"
+ "Bárki kérheti, hogy csatlakozzon a szobához, de egy adminisztrátornak vagy moderátornak el kell fogadnia a kérést."
+ "Csatlakozás kérése"
+ "Igen, engedélyezze a titkosítást"
+ "Az engedélyezés után a szoba titkosítása nem tiltható le. Az üzenetek előzményei csak a szobatagok számára láthatók, amikor meghívást kaptak, vagy mióta csatlakoztak a szobához.
+A szobatagokon kívül senki sem tudja olvasni az üzeneteket. Ez megakadályozhatja a botok és a hidak megfelelő működését.
+Nem javasoljuk a titkosítás engedélyezését az olyan szobákban, amelyeket bárki megtalálhat és csatlakozhat."
+ "Engedélyezi a titkosítást?"
+ "Engedélyezés után a titkosítás nem tiltható le."
+ "Titkosítás"
+ "Végpontok közötti titkosítás engedélyezése"
+ "Bárki megtalálhatja és csatlakozhat"
+ "Bárki"
+ "Az emberek csak akkor csatlakozhatnak, ha meghívást kapnak"
+ "Csak meghívással"
+ "Szobahozzáférés"
+ "Biztonság és adatvédelem"
"Hely megosztása"
"Saját hely megosztása"
"Megnyitás az Apple Mapsben"
@@ -331,4 +358,8 @@ Ok: %1$s."
"Hely"
"Verzió: %1$s (%2$s)"
"hu"
+ "A korábbi üzenetek nem érhetők el ezen az eszközön"
+ "Nincs hozzáférése ehhez az üzenethez"
+ "Nem sikerült visszafejteni az üzenetet"
+ "Ez az üzenet azért lett blokkolva, mert vagy nem ellenőrizte az eszközt, vagy a feladónak ellenőriznie kell az Ön személyazonosságát."
diff --git a/libraries/ui-strings/src/main/res/values-in/translations.xml b/libraries/ui-strings/src/main/res/values-in/translations.xml
index 3755164173a..5cf002c3342 100644
--- a/libraries/ui-strings/src/main/res/values-in/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-in/translations.xml
@@ -305,7 +305,6 @@ Alasan: %1$s."
"Pesan Anda tidak terkirim karena %1$s belum memverifikasi semua perangkat"
"Satu atau beberapa perangkat Anda tidak terverifikasi. Anda tetap dapat mengirim pesan, atau Anda dapat membatalkannya dan mencoba lagi nanti setelah Anda memverifikasi semua perangkat."
"Pesan Anda tidak terkirim karena Anda belum memverifikasi satu atau beberapa perangkat Anda"
- "Pesan yang disematkan"
"Gagal memproses media untuk diunggah, silakan coba lagi."
"Tidak dapat mengambil detail pengguna"
"%1$s dari %2$s"
diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml
index 529fc66c06f..68547a7f7e9 100644
--- a/libraries/ui-strings/src/main/res/values-it/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-it/translations.xml
@@ -32,6 +32,7 @@
"Registra un messaggio vocale."
"Ferma la registrazione"
"Accetta"
+ "Aggiungi didascalia"
"Aggiungi alla conversazione"
"Indietro"
"Chiama"
@@ -45,8 +46,10 @@
"Conferma password"
"Continua"
"Copia"
+ "Copia didascalia"
"Copia collegamento"
"Copia collegamento al messaggio"
+ "Copia testo"
"Crea"
"Crea una stanza"
"Disattiva"
@@ -57,6 +60,7 @@
"Annulla"
"Fine"
"Modifica"
+ "Modifica didascalia"
"Modifica sondaggio"
"Attiva"
"Termina sondaggio"
@@ -64,6 +68,7 @@
"Password dimenticata?"
"Inoltra"
"Indietro"
+ "Ignora"
"Invita"
"Invita persone"
"Invita persone su %1$s"
@@ -84,12 +89,14 @@
"OK"
"Impostazioni"
"Apri con"
- "Pin"
+ "Fissa"
"Risposta rapida"
"Citazione"
"Reagisci"
"Rifiuta"
"Rimuovi"
+ "Rimuovi didascalia"
+ "Rimuovi messaggio"
"Rispondi"
"Rispondi nella discussione"
"Segnala un problema"
@@ -104,6 +111,7 @@
"Invia messaggio"
"Condividi"
"Condividi collegamento"
+ "Mostra"
"Accedi di nuovo"
"Disconnetti"
"Disconnetti comunque"
@@ -121,6 +129,7 @@
"Sì"
"Informazioni"
"Regole sull\'utilizzo consentito"
+ "Aggiunta didascalia"
"Impostazioni avanzate"
"Statistiche di utilizzo"
"Aspetto"
@@ -136,11 +145,14 @@
"Scuro"
"Errore di decrittazione"
"Opzioni sviluppatore"
+ "ID dispositivo"
"Conversazione diretta"
"Non mostrarlo più"
"(modificato)"
"Modifica in corso"
+ "Modifica didascalia"
"* %1$s %2$s"
+ "Crittografia"
"Crittografia abilitata"
"Inserisci il PIN"
"Errore"
@@ -154,6 +166,7 @@ Motivo:. %1$s"
"File"
"File salvato"
"Inoltra messaggio"
+ "Usati di frequente"
"GIF"
"Immagine"
"In risposta a %1$s"
@@ -234,22 +247,30 @@ Motivo:. %1$s"
"Argomento"
"Di cosa parla questa stanza?"
"Impossibile decrittografare"
+ "Inviato da un dispositivo non sicuro"
"Non hai accesso a questo messaggio"
+ "L\'identità verificata del mittente è cambiata"
"Non è stato possibile spedire inviti a uno o più utenti."
"Impossibile inviare inviti"
"Sblocca"
"Annulla silenzioso"
+ "Chiamata non supportata"
"Evento non supportato"
"Nome utente"
"Verifica annullata"
"Verifica completata"
+ "Verifica fallita"
+ "Verificato"
"Verifica dispositivo"
+ "Verifica l\'identità"
"Video"
"Messaggio vocale"
"In attesa…"
"In attesa del messaggio"
"Tu"
"L\'identità di %1$s sembra essere cambiata. %2$s"
+ "L\'identità di %1$s %2$s sembra essere cambiata. %3$s"
+ "(%1$s)"
"Conferma"
"Errore"
"Operazione riuscita"
@@ -279,6 +300,7 @@ Motivo:. %1$s"
"%1$s Android"
"Scuoti per segnalare un problema"
"Selezione del file multimediale fallita, riprova."
+ "Le didascalie potrebbero non essere visibili agli utenti di app meno recenti."
"Elaborazione del file multimediale da caricare fallita, riprova."
"Caricamento del file multimediale fallito, riprova."
"Premi su un messaggio e scegli “%1$s” per includerlo qui."
@@ -298,7 +320,6 @@ Motivo:. %1$s"
"Il tuo messaggio non è stato inviato perché %1$s non ha verificato tutti i dispositivi."
"Uno o più dispositivi non sono verificati. Puoi inviare il messaggio comunque, oppure annullarlo e riprovare più tardi dopo aver verificato tutti i tuoi dispositivi."
"Il tuo messaggio non è stato inviato perché non hai verificato uno o più dispositivi."
- "Messaggi fissati"
"Elaborazione del file multimediale da caricare fallita, riprova."
"Impossibile recuperare i dettagli dell\'utente"
"%1$s di %2$s"
@@ -306,6 +327,7 @@ Motivo:. %1$s"
"Caricamento messaggio…"
"Mostra tutti"
"Conversazione"
+ "Richiesta di accesso inviata"
"Condividi posizione"
"Condividi la mia posizione"
"Apri in Apple Maps"
@@ -318,4 +340,7 @@ Motivo:. %1$s"
"Posizione"
"Versione: %1$s (%2$s)"
"it"
+ "La cronologia messaggi non è disponibile su questo dispositivo"
+ "Impossibile decifrare il messaggio"
+ "Questo messaggio è stato bloccato perché il dispositivo non è verificato o perché il mittente deve verificare la tua identità."
diff --git a/libraries/ui-strings/src/main/res/values-nl/translations.xml b/libraries/ui-strings/src/main/res/values-nl/translations.xml
index 71f173b9bfc..b9a672e706b 100644
--- a/libraries/ui-strings/src/main/res/values-nl/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-nl/translations.xml
@@ -302,7 +302,6 @@ Reden: %1$s."
"Je bericht is niet verzonden omdat %1$s niet alle apparaten heeft geverifieerd"
"Een of meer van je apparaten zijn niet geverifieerd. Je kunt het bericht toch verzenden, of je kunt het voorlopig annuleren en het later opnieuw proberen nadat je al je apparaten hebt geverifieerd."
"Je bericht is niet verzonden omdat je een of meerdere apparaten niet geverifieerd hebt"
- "Vastgezette berichten"
"Het verwerken van media voor uploaden is mislukt. Probeer het opnieuw."
"Kon gebruikersgegevens niet ophalen"
"%1$s van %2$s"
diff --git a/libraries/ui-strings/src/main/res/values-pl/translations.xml b/libraries/ui-strings/src/main/res/values-pl/translations.xml
index 6373c2fec65..3d84d351a8d 100644
--- a/libraries/ui-strings/src/main/res/values-pl/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml
@@ -126,7 +126,7 @@
"O programie"
"Polityka użytkowania"
"Ustawienia zaawansowane"
- "Analityka"
+ "Dane analityczne"
"Wygląd"
"Dźwięk"
"Zablokowani użytkownicy"
@@ -262,6 +262,7 @@ Powód: %1$s."
"Oczekiwanie na tę wiadomość"
"Ty"
"Tożsamość %1$s mogła ulec zmianie. %2$s"
+ "Wygląda na to, że tożsamość %1$s %2$s uległa zmianie. %3$s"
"(%1$s)"
"Potwierdzenie"
"Błąd"
@@ -312,7 +313,6 @@ Powód: %1$s."
"Twoja wiadomość nie została wysłana, ponieważ %1$s nie zweryfikował swoich wszystkich urządzeń"
"Jedno lub więcej z Twoich urządzeń jest niezweryfikowanych. Wyślij wiadomość mimo to lub anuluj i spróbuj ponownie po zweryfikowaniu wszystkich swoich urządzeń."
"Twoja wiadomość nie została wysłana, ponieważ nie zweryfikowałeś jednego lub więcej swoich urządzeń."
- "Przypięte wiadomości"
"Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie."
"Nie można pobrać danych użytkownika"
"%1$s z %2$s"
diff --git a/libraries/ui-strings/src/main/res/values-pt/translations.xml b/libraries/ui-strings/src/main/res/values-pt/translations.xml
index 437d6b63e6b..967647b836b 100644
--- a/libraries/ui-strings/src/main/res/values-pt/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-pt/translations.xml
@@ -310,7 +310,6 @@ Razão: %1$s."
"A sua mensagem não foi enviada porque %1$s não verificou todos os dispositivos"
"Um ou mais dos teus dispositivos não foram verificados. Podes enviar a mensagem na mesma, ou podes cancelar por agora e tentar novamente mais tarde, depois de teres verificado todos os teus dispositivos."
"A sua mensagem não foi enviada porque não verificou um ou mais dos seus dispositivos"
- "Mensagens afixadas"
"Falha ao processar multimédia para carregamento, por favor tente novamente."
"Não foi possível obter os detalhes de utilizador."
"%1$s de %2$s"
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 35e9388ae76..2b18c440440 100644
--- a/libraries/ui-strings/src/main/res/values-ru/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-ru/translations.xml
@@ -150,6 +150,7 @@
"Идентификатор устройства"
"Личный чат"
"Не показывать больше"
+ "Загрузка"
"(изменено)"
"Редактирование"
"Редактирование подписи"
@@ -303,20 +304,6 @@
"Привет, поговори со мной по %1$s: %2$s"
"%1$s Android"
"Встряхните устройство, чтобы сообщить об ошибке"
- "Да, принять все"
- "Вы действительно хотите принять все заявки на присоединение?"
- "Принять все запросы"
- "Принять всё"
- "Да, отклонить и запретить"
- "Вы уверен, что хочешь отклонить и запретить %1$s? Этот пользователь больше не сможет запросить доступ к этой комнате."
- "Отклонить и запретить доступ"
- "Да, отклонить"
- "Вы уверены, что хотите отклонить %1$s запрос на присоединение к этой комнате?"
- "Отклонить доступ"
- "Отклонить и запретить"
- "Вы сможете увидеть запрос, когда кто-то попросит присоединиться к комнате."
- "Нет ожидающих запросов на присоединение"
- "Запросы на присоединение"
"Не удалось выбрать носитель, попробуйте еще раз."
"Подпись может быть не видна пользователям старых приложений."
"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."
@@ -339,23 +326,12 @@
"Ваше сообщение не было отправлено, потому что %1$s не проверил одно или несколько устройств"
"Одно или несколько ваших устройств не проверены. Вы можете отправить сообщение в любом случае или отменить его пока и повторить попытку позже, проверив все свои устройства."
"Ваше сообщение не было отправлено, поскольку вы не подтвердили одно или несколько своих устройств."
- "Закрепленные сообщения"
- "Запросы на вступление"
"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."
"Не удалось получить данные о пользователе"
-
- - "%1$s +%2$d хочет присоединиться к этой комнате"
- - "%1$s +%2$d хотят присоединиться к этой комнате"
- - "%1$s +%2$d хотят присоединиться к этой комнате"
-
- "Показать все"
"%1$s из %2$s"
"%1$s Закрепленные сообщения"
"Загрузка сообщения…"
"Посмотреть все"
- "Принять"
- "%1$s хочет присоединиться к этой комнате"
- "Просмотр"
"Чат"
"Запрос на присоединение отправлен"
"Поделиться местоположением"
@@ -370,4 +346,8 @@
"Местоположение"
"Версия: %1$s (%2$s)"
"ru"
+ "На этом устройстве недоступна история сообщений"
+ "У вас нет доступа к этому сообщению"
+ "Не удалось расшифровать сообщение"
+ "Это сообщение было заблокировано по причине того, что вы не подтвердили свое устройство, либо отправителю необходимо подтвердить вашу личность."
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 64646a62457..1c26d0eaae3 100644
--- a/libraries/ui-strings/src/main/res/values-sk/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml
@@ -300,11 +300,6 @@ Dôvod: %1$s."
"Ahoj, porozprávajte sa so mnou na %1$s: %2$s"
"%1$s Android"
"Zúrivo potriasť pre nahlásenie chyby"
- "Prijať všetky"
- "Odmietnuť a zakázať"
- "Keď niekto požiada, aby sa pripojil k miestnosti, jeho žiadosť si môžete pozrieť tu."
- "Žiadna čakajúca žiadosť o pripojenie"
- "Žiadosti o pripojenie"
"Nepodarilo sa vybrať médium, skúste to prosím znova."
"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."
"Nepodarilo sa nahrať médiá, skúste to prosím znova."
@@ -326,23 +321,12 @@ Dôvod: %1$s."
"Vaša správa nebola odoslaná, pretože %1$s neoveril/a všetky zariadenia."
"Jedno alebo viac vašich zariadení nie je overených. Správu môžete odoslať aj tak, alebo môžete zatiaľ zrušiť a skúsiť to znova neskôr po overení všetkých svojich zariadení."
"Vaša správa nebola odoslaná, pretože ste neoverili jedno alebo viac svojich zariadení"
- "Pripnuté správy"
- "Žiadosti o vstup"
"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."
"Nepodarilo sa získať údaje o používateľovi"
-
- - "%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"
- - "%1$s +%2$d ďalší chcú vstúpiť do tejto miestnosti"
- - "%1$s +%2$d ďalších chce vstúpiť do tejto miestnosti"
-
- "Zobraziť všetko"
"%1$s z %2$s"
"%1$s Pripnutých správ"
"Načítava sa správa…"
"Zobraziť všetko"
- "Prijať"
- "%1$s chce vstúpiť do tejto miestnosti"
- "Zobraziť"
"Konverzácia"
"Žiadosť o vstup odoslaná"
"Zdieľať polohu"
diff --git a/libraries/ui-strings/src/main/res/values-sv/translations.xml b/libraries/ui-strings/src/main/res/values-sv/translations.xml
index 8d3b71030ad..5fe04394f86 100644
--- a/libraries/ui-strings/src/main/res/values-sv/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-sv/translations.xml
@@ -282,7 +282,6 @@ Anledning:%1$s."
"Fästa meddelanden"
"Du är på väg att gå till ditt %1$s-konto för att återställa din identitet. Därefter kommer du att tas tillbaka till appen."
"Kan du inte bekräfta? Gå till ditt konto för att återställa din identitet."
- "Fästa meddelanden"
"Misslyckades att bearbeta media för uppladdning, vänligen pröva igen."
"Kunde inte hämta användarinformation"
"%1$s av %2$s"
diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml
index 5c86a279594..15506c139c8 100644
--- a/libraries/ui-strings/src/main/res/values-uk/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml
@@ -315,7 +315,6 @@
"Ваше повідомлення не було надіслано, тому що %1$s не перевірив усі пристрої"
"Один або кілька ваших пристроїв не підтверджено. Ви можете відправити повідомлення в будь-якому випадку, або ж скасувати відправку і спробувати пізніше, коли перевірите всі свої пристрої."
"Ваше повідомлення не було надіслано, оскільки ви не підтвердили один або декілька своїх пристроїв"
- "Закріплені повідомлення"
"Не вдалося обробити медіафайл для завантаження, спробуйте ще раз."
"Не вдалося отримати дані користувача"
"%1$s із %2$s"
diff --git a/libraries/ui-strings/src/main/res/values-zh/translations.xml b/libraries/ui-strings/src/main/res/values-zh/translations.xml
index 80e69850ff7..0451985bb2f 100644
--- a/libraries/ui-strings/src/main/res/values-zh/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml
@@ -305,7 +305,6 @@
"您的消息未发送,因为%1$s尚未验证所有设备"
"您有未验证的设备。您仍然可以发送消息;也可以暂时取消,并在验证所有设备后稍后重试。"
"您的消息未发送,因为您有尚未验证的设备。"
- "置顶消息"
"处理要上传的媒体失败,请重试。"
"无法获取用户信息"
"%1$s / %2$s"
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index 8f4aa8b20c2..1c68bef70aa 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -127,6 +127,7 @@
"View in timeline"
"View source"
"Yes"
+ "Yes, try again"
"About"
"Acceptable use policy"
"Adding caption"
@@ -148,6 +149,7 @@
"Device ID"
"Direct chat"
"Do not show this again"
+ "Downloading"
"(edited)"
"Editing"
"Editing caption"
@@ -299,20 +301,6 @@ Reason: %1$s."
"Hey, talk to me on %1$s: %2$s"
"%1$s Android"
"Rageshake to report bug"
- "Yes, accept all"
- "Are you sure you want to accept all requests to join?"
- "Accept all requests"
- "Accept all"
- "Yes, decline and ban"
- "Are you sure you want to decline and ban %1$s? This user won’t be able to request access to join this room again."
- "Decline and ban from accessing"
- "Yes, decline"
- "Are you sure you want to decline %1$s request to join this room?"
- "Decline access"
- "Decline and ban"
- "When somebody will ask to join the room, you’ll be able to see their request here."
- "No pending request to join"
- "Requests to join"
"Failed selecting media, please try again."
"Captions might not be visible to people using older apps."
"Failed processing media to upload, please try again."
@@ -334,24 +322,30 @@ Reason: %1$s."
"Your message was not sent because %1$s has not verified all devices"
"One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."
"Your message was not sent because you have not verified one or more of your devices"
- "Pinned messages"
- "Requests to join"
"Failed processing media to upload, please try again."
"Could not retrieve user details"
-
- - "%1$s +%2$d other want to join this room"
- - "%1$s +%2$d others want to join this room"
-
- "View all"
"%1$s of %2$s"
"%1$s Pinned messages"
"Loading message…"
"View All"
- "Accept"
- "%1$s wants to join this room"
- "View"
"Chat"
"Request to join sent"
+ "Anyone can ask to join the room but an administrator or moderator will have to accept the request."
+ "Ask to join"
+ "Yes, enable encryption"
+ "Once enabled, encryption for a room cannot be disabled, Message history will only be visible for room members since they were invited or since they joined the room.
+No one besides the room members will be able to read messages. This may prevent bots and bridges to work correctly.
+We do not recommend enabling encryption for rooms that anyone can find and join."
+ "Enable encryption?"
+ "Once enabled, encryption cannot be disabled."
+ "Encryption"
+ "Enable end-to-end encryption"
+ "Anyone can find and join"
+ "Anyone"
+ "People can only join if they are invited"
+ "Invite only"
+ "Room access"
+ "Security & privacy"
"Share location"
"Share my location"
"Open in Apple Maps"
@@ -366,6 +360,8 @@ Reason: %1$s."
"en"
"en"
"Historical messages are not available on this device"
+ "You need to verify this device for access to historical messages"
+ "You don\'t have access to this message"
"Unable to decrypt message"
"This message was blocked either because you did not verify your device or because the sender needs to verify your identity."
diff --git a/libraries/voiceplayer/api/build.gradle.kts b/libraries/voiceplayer/api/build.gradle.kts
new file mode 100644
index 00000000000..5beb8ebbc07
--- /dev/null
+++ b/libraries/voiceplayer/api/build.gradle.kts
@@ -0,0 +1,23 @@
+import extension.setupAnvil
+
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.libraries.voiceplayer.api"
+}
+
+setupAnvil()
+
+dependencies {
+ implementation(libs.androidx.annotationjvm)
+ implementation(libs.coroutines.core)
+ implementation(projects.libraries.matrix.api)
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageEvents.kt b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageEvents.kt
similarity index 80%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageEvents.kt
rename to libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageEvents.kt
index d124e57dcc0..4ea61b85474 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageEvents.kt
+++ b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageEvents.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.api
sealed interface VoiceMessageEvents {
data object PlayPause : VoiceMessageEvents
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageException.kt
similarity index 82%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt
rename to libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageException.kt
index ff3c5542f6d..c35ec0b14ce 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt
+++ b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageException.kt
@@ -5,17 +5,19 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages
+package io.element.android.libraries.voiceplayer.api
-internal sealed class VoiceMessageException : Exception() {
+sealed class VoiceMessageException : Exception() {
data class FileException(
override val message: String?,
override val cause: Throwable? = null
) : VoiceMessageException()
+
data class PermissionMissing(
override val message: String?,
override val cause: Throwable?
) : VoiceMessageException()
+
data class PlayMessageError(
override val message: String?,
override val cause: Throwable?
diff --git a/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessagePresenterFactory.kt b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessagePresenterFactory.kt
new file mode 100644
index 00000000000..1e5c706b108
--- /dev/null
+++ b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessagePresenterFactory.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.voiceplayer.api
+
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import kotlin.time.Duration
+
+interface VoiceMessagePresenterFactory {
+ fun createVoiceMessagePresenter(
+ eventId: EventId?,
+ mediaSource: MediaSource,
+ mimeType: String?,
+ filename: String?,
+ duration: Duration,
+ ): Presenter
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageState.kt b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageState.kt
similarity index 86%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageState.kt
rename to libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageState.kt
index a7d0c15c133..5200614d571 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageState.kt
+++ b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageState.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.api
data class VoiceMessageState(
val button: Button,
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStateProvider.kt
similarity index 95%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt
rename to libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStateProvider.kt
index 75d00240a27..a06181a4ee8 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageStateProvider.kt
+++ b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStateProvider.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.api
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
diff --git a/libraries/voiceplayer/impl/build.gradle.kts b/libraries/voiceplayer/impl/build.gradle.kts
new file mode 100644
index 00000000000..155190e3bb1
--- /dev/null
+++ b/libraries/voiceplayer/impl/build.gradle.kts
@@ -0,0 +1,43 @@
+import extension.setupAnvil
+
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+plugins {
+ id("io.element.android-compose-library")
+}
+
+android {
+ namespace = "io.element.android.libraries.voiceplayer.impl"
+}
+
+setupAnvil()
+
+dependencies {
+ api(projects.libraries.voiceplayer.api)
+
+ implementation(projects.libraries.core)
+ implementation(projects.libraries.di)
+ implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.mediaplayer.api)
+ implementation(projects.libraries.uiUtils)
+ implementation(projects.services.analytics.api)
+
+ implementation(libs.androidx.annotationjvm)
+ implementation(libs.coroutines.core)
+
+ testImplementation(libs.molecule.runtime)
+ testImplementation(libs.test.junit)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.mockk)
+ testImplementation(libs.test.turbine)
+ testImplementation(libs.coroutines.core)
+ testImplementation(libs.coroutines.test)
+ testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.mediaplayer.test)
+ testImplementation(projects.services.analytics.test)
+ testImplementation(projects.tests.testutils)
+}
diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt
new file mode 100644
index 00000000000..48807f50271
--- /dev/null
+++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.voiceplayer.impl
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
+import io.element.android.services.analytics.api.AnalyticsService
+import kotlinx.coroutines.CoroutineScope
+import javax.inject.Inject
+import kotlin.time.Duration
+
+@ContributesBinding(RoomScope::class)
+class DefaultVoiceMessagePresenterFactory @Inject constructor(
+ private val analyticsService: AnalyticsService,
+ private val scope: CoroutineScope,
+ private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
+) : VoiceMessagePresenterFactory {
+ override fun createVoiceMessagePresenter(
+ eventId: EventId?,
+ mediaSource: MediaSource,
+ mimeType: String?,
+ filename: String?,
+ duration: Duration,
+ ): Presenter {
+ val player = voiceMessagePlayerFactory.create(
+ eventId = eventId,
+ mediaSource = mediaSource,
+ mimeType = mimeType,
+ filename = filename,
+ )
+
+ return VoiceMessagePresenter(
+ analyticsService = analyticsService,
+ scope = scope,
+ player = player,
+ eventId = eventId,
+ duration = duration,
+ )
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt
similarity index 98%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt
rename to libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt
index f1d8e5f9875..71357a2559b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageMediaRepo.kt
+++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.impl
import com.squareup.anvil.annotations.ContributesBinding
import dagger.assisted.Assisted
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt
similarity index 98%
rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt
rename to libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt
index aa339e33655..308edd0a51d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePlayer.kt
+++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.mimetype.MimeTypes
diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt
new file mode 100644
index 00000000000..0786d2d7ed7
--- /dev/null
+++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.voiceplayer.impl
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.runUpdatingState
+import io.element.android.libraries.core.extensions.flatMap
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.ui.utils.time.formatShort
+import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
+import io.element.android.libraries.voiceplayer.api.VoiceMessageException
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
+import io.element.android.services.analytics.api.AnalyticsService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+
+class VoiceMessagePresenter(
+ private val analyticsService: AnalyticsService,
+ private val scope: CoroutineScope,
+ private val player: VoiceMessagePlayer,
+ private val eventId: EventId?,
+ private val duration: Duration,
+) : Presenter {
+ private val play = mutableStateOf>(AsyncData.Uninitialized)
+
+ @Composable
+ override fun present(): VoiceMessageState {
+ val playerState by player.state.collectAsState(
+ VoiceMessagePlayer.State(
+ isReady = false,
+ isPlaying = false,
+ isEnded = false,
+ currentPosition = 0L,
+ duration = null
+ )
+ )
+
+ val button by remember {
+ derivedStateOf {
+ when {
+ eventId == null -> VoiceMessageState.Button.Disabled
+ playerState.isPlaying -> VoiceMessageState.Button.Pause
+ play.value is AsyncData.Loading -> VoiceMessageState.Button.Downloading
+ play.value is AsyncData.Failure -> VoiceMessageState.Button.Retry
+ else -> VoiceMessageState.Button.Play
+ }
+ }
+ }
+ val duration by remember {
+ derivedStateOf { playerState.duration ?: duration.inWholeMilliseconds }
+ }
+ val progress by remember {
+ derivedStateOf {
+ playerState.currentPosition / duration.toFloat()
+ }
+ }
+ val time by remember {
+ derivedStateOf {
+ when {
+ playerState.isReady && !playerState.isEnded -> playerState.currentPosition
+ playerState.currentPosition > 0 -> playerState.currentPosition
+ else -> duration
+ }.milliseconds.formatShort()
+ }
+ }
+ val showCursor by remember {
+ derivedStateOf {
+ !play.value.isUninitialized() && !playerState.isEnded
+ }
+ }
+
+ fun eventSink(event: VoiceMessageEvents) {
+ when (event) {
+ is VoiceMessageEvents.PlayPause -> {
+ if (playerState.isPlaying) {
+ player.pause()
+ } else if (playerState.isReady) {
+ player.play()
+ } else {
+ scope.launch {
+ play.runUpdatingState(
+ errorTransform = {
+ analyticsService.trackError(
+ VoiceMessageException.PlayMessageError("Error while trying to play voice message", it)
+ )
+ it
+ },
+ ) {
+ player.prepare().flatMap {
+ runCatching { player.play() }
+ }
+ }
+ }
+ }
+ }
+ is VoiceMessageEvents.Seek -> {
+ player.seekTo((event.percentage * duration).toLong())
+ }
+ }
+ }
+
+ return VoiceMessageState(
+ button = button,
+ progress = progress,
+ time = time,
+ showCursor = showCursor,
+ eventSink = { eventSink(it) },
+ )
+ }
+}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt
similarity index 98%
rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt
rename to libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt
index fcf19980973..4c7b176fa56 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessageMediaRepoTest.kt
+++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessageMediaRepoTest.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.mimetype.MimeTypes
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePlayerTest.kt
similarity index 99%
rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt
rename to libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePlayerTest.kt
index 9a82b467768..fdc9b2ee502 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/DefaultVoiceMessagePlayerTest.kt
+++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePlayerTest.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.impl
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/FakeVoiceMessageMediaRepo.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/FakeVoiceMessageMediaRepo.kt
similarity index 89%
rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/FakeVoiceMessageMediaRepo.kt
rename to libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/FakeVoiceMessageMediaRepo.kt
index 8d2f5b88acf..8867af82874 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/FakeVoiceMessageMediaRepo.kt
+++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/FakeVoiceMessageMediaRepo.kt
@@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.impl
import io.element.android.tests.testutils.simulateLongTask
import java.io.File
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenterTest.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt
similarity index 85%
rename from features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenterTest.kt
rename to libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt
index ceedf0948fb..59b18919625 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenterTest.kt
+++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt
@@ -5,21 +5,25 @@
* Please see LICENSE in the repository root for full details.
*/
-package io.element.android.features.messages.impl.voicemessages.timeline
+package io.element.android.libraries.voiceplayer.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
-import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
-import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
+import io.element.android.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.mediaplayer.test.FakeMediaPlayer
+import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
+import io.element.android.libraries.voiceplayer.api.VoiceMessageException
+import io.element.android.libraries.voiceplayer.api.VoiceMessageState
import io.element.android.services.analytics.api.AnalyticsService
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
import kotlin.time.Duration.Companion.milliseconds
class VoiceMessagePresenterTest {
@@ -41,7 +45,7 @@ class VoiceMessagePresenterTest {
fun `pressing play downloads and plays`() = runTest {
val presenter = createVoiceMessagePresenter(
mediaPlayer = FakeMediaPlayer(fakeTotalDurationMs = 2_000),
- content = aTimelineItemVoiceContent(duration = 2_000.milliseconds),
+ duration = 2_000.milliseconds,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -79,7 +83,7 @@ class VoiceMessagePresenterTest {
mediaPlayer = FakeMediaPlayer(fakeTotalDurationMs = 2_000),
voiceMessageMediaRepo = FakeVoiceMessageMediaRepo().apply { shouldFail = true },
analyticsService = analyticsService,
- content = aTimelineItemVoiceContent(duration = 2_000.milliseconds),
+ duration = 2_000.milliseconds,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -115,7 +119,7 @@ class VoiceMessagePresenterTest {
fun `pressing pause while playing pauses`() = runTest {
val presenter = createVoiceMessagePresenter(
mediaPlayer = FakeMediaPlayer(fakeTotalDurationMs = 2_000),
- content = aTimelineItemVoiceContent(duration = 2_000.milliseconds),
+ duration = 2_000.milliseconds,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -147,7 +151,7 @@ class VoiceMessagePresenterTest {
@Test
fun `content with null eventId shows disabled button`() = runTest {
val presenter = createVoiceMessagePresenter(
- content = aTimelineItemVoiceContent(eventId = null),
+ eventId = null,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -164,7 +168,7 @@ class VoiceMessagePresenterTest {
fun `seeking before play`() = runTest {
val presenter = createVoiceMessagePresenter(
mediaPlayer = FakeMediaPlayer(fakeTotalDurationMs = 2_000),
- content = aTimelineItemVoiceContent(duration = 10_000.milliseconds),
+ duration = 10_000.milliseconds,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -188,7 +192,7 @@ class VoiceMessagePresenterTest {
@Test
fun `seeking after play`() = runTest {
val presenter = createVoiceMessagePresenter(
- content = aTimelineItemVoiceContent(duration = 10_000.milliseconds),
+ duration = 10_000.milliseconds,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -224,19 +228,23 @@ fun TestScope.createVoiceMessagePresenter(
mediaPlayer: FakeMediaPlayer = FakeMediaPlayer(),
voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(),
analyticsService: AnalyticsService = FakeAnalyticsService(),
- content: TimelineItemVoiceContent = aTimelineItemVoiceContent(),
+ eventId: EventId? = EventId("\$anEventId"),
+ filename: String = "filename doesn't really matter for a voice message",
+ duration: Duration = 61_000.milliseconds,
+ contentUri: String = "mxc://matrix.org/1234567890abcdefg",
+ mimeType: String = MimeTypes.Ogg,
+ mediaSource: MediaSource = MediaSource(contentUri),
) = VoiceMessagePresenter(
- voiceMessagePlayerFactory = { eventId, mediaSource, mimeType, filename ->
- DefaultVoiceMessagePlayer(
- mediaPlayer = mediaPlayer,
- voiceMessageMediaRepoFactory = { _, _, _ -> voiceMessageMediaRepo },
- eventId = eventId,
- mediaSource = mediaSource,
- mimeType = mimeType,
- filename = filename
- )
- },
analyticsService = analyticsService,
scope = this,
- content = content,
+ player = DefaultVoiceMessagePlayer(
+ mediaPlayer = mediaPlayer,
+ voiceMessageMediaRepoFactory = { _, _, _ -> voiceMessageMediaRepo },
+ eventId = eventId,
+ mediaSource = mediaSource,
+ mimeType = mimeType,
+ filename = filename
+ ),
+ eventId = eventId,
+ duration = duration,
)
diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt
index 2730a94337b..fe63c2c68e5 100644
--- a/plugins/src/main/kotlin/Versions.kt
+++ b/plugins/src/main/kotlin/Versions.kt
@@ -47,7 +47,7 @@ private const val versionMinor = 7
// Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release.
-private const val versionPatch = 5
+private const val versionPatch = 6
object Versions {
const val VERSION_CODE = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch
diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
index f54cdb81cae..4d24ffebfd2 100644
--- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
+++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
@@ -83,6 +83,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:textcomposer:impl"))
implementation(project(":libraries:roomselect:impl"))
implementation(project(":libraries:cryptography:impl"))
+ implementation(project(":libraries:voiceplayer:impl"))
implementation(project(":libraries:voicerecorder:impl"))
implementation(project(":libraries:mediaplayer:impl"))
implementation(project(":libraries:mediaviewer:impl"))
diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_de.png
new file mode 100644
index 00000000000..037c8ce9a10
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b8f78e95438b2d7fe2712683ffb692c8d7069f1395b28f9c87f4bef82f17d5a7
+size 32384
diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_de.png
new file mode 100644
index 00000000000..3b0faba9113
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fcdd1ec52cbe0db11941ce6a7a533053ab532b5acc0e6ea1b2f69a744dc4e0ac
+size 37972
diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_de.png
new file mode 100644
index 00000000000..34c04cbfd4f
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fc19b12b6617d7d8a2209cf80b27b4183c53a83b232060c0c8aef7cd4e3df25c
+size 21093
diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_de.png
new file mode 100644
index 00000000000..5c01877a844
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:34a6e5bee161e910ff97c7925f49e52c416480abbd5bd19228a27fa8b0d917ce
+size 21802
diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_de.png
new file mode 100644
index 00000000000..5688c7d06ac
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9a1c367f94edbdac729a3ed3bb98dca74ec5ce5264b25f8d102a2be0bc2ae9c3
+size 29342
diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_de.png
new file mode 100644
index 00000000000..037c8ce9a10
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b8f78e95438b2d7fe2712683ffb692c8d7069f1395b28f9c87f4bef82f17d5a7
+size 32384
diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_de.png
new file mode 100644
index 00000000000..037c8ce9a10
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b8f78e95438b2d7fe2712683ffb692c8d7069f1395b28f9c87f4bef82f17d5a7
+size 32384
diff --git a/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_7_de.png b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_7_de.png
new file mode 100644
index 00000000000..12c9aaa2d6e
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_7_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b9b9a318514fcbf0bb6de4fd8c45f7d30d0044a085db23972cfd05c96cca1350
+size 42207
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_0_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_0_de.png
new file mode 100644
index 00000000000..134e9bdab99
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_0_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9620eeb151631783e732ac036c562887b49ae075f48f579f7c83cf05ee982ecf
+size 8262
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_1_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_1_de.png
new file mode 100644
index 00000000000..cf7480bdacf
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_1_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2660a313aa5910965ea16ed0f522aa6b3d9ba338ad1982b6f56d8ffbc0540b8
+size 29320
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_2_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_2_de.png
new file mode 100644
index 00000000000..5dc14a3d58c
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_2_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e87303b2679ab4315c4f4357a1602b61fd48a28757cd9424cff6eeffca589839
+size 34791
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_3_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_3_de.png
new file mode 100644
index 00000000000..06b28f4d824
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_3_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9459af0c6974c83ba734436c42574a3894caddc07daf4dd54c18ba17b8e5c3a5
+size 43379
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_4_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_4_de.png
new file mode 100644
index 00000000000..d22b9672914
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_4_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f93153775942f24e599c8a57ca3bb5b8f3924e7717b647c7f10cc10d9a44e4fb
+size 55896
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_5_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_5_de.png
new file mode 100644
index 00000000000..2db28246d3f
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_5_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:86f1d4fc547228af5e380f016751e886d15ebf0bf08b3248c8d6ffcca4a77109
+size 31542
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_6_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_6_de.png
new file mode 100644
index 00000000000..2cf7dca91fa
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_6_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:764b6feaeabb60fc0d77978af75740a935351df26049824658bdefb6e4853074
+size 31763
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_7_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_7_de.png
new file mode 100644
index 00000000000..0af2113d9cd
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_7_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aa4db76cd0989126176b4e972f142ffed85a6986aeaccfb238ec090de4b9872d
+size 31844
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_8_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_8_de.png
new file mode 100644
index 00000000000..df489c8ea1c
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_8_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0239309f37dd691f9e4c17705e4a5124d271684e9a578b10bd7e2463b9b2df18
+size 28380
diff --git a/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_9_de.png b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_9_de.png
new file mode 100644
index 00000000000..d97d375fea7
--- /dev/null
+++ b/screenshots/de/features.knockrequests.impl.list_KnockRequestsListView_Day_9_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b49e768d7ece4275118c3cc394d9f9cd93c0e62ccbef649f71554a1124572450
+size 31549
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png
index b0bf21949f7..7e30e444ff7 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_10_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:85b0a307f751b991e16ec61e27222fb3a51808c1ea81869eb3513925c18977ee
-size 27513
+oid sha256:ef6458d23f92e73f47d54f82ac38ec090f2f7663e353972bdb4581fd69911711
+size 30604
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png
index 10051a39ee1..cfe31bc8fee 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_11_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0ab5fd552ac94ea96da1403099877e171751d8b4521f425041407c59f3d659cd
-size 52022
+oid sha256:3b03b6a9c407472a2680e86ea89fb8a1839fccf694e2038b419e3b304b2a7340
+size 54205
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png
index fff94dc871d..4cb71805e1c 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_12_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dc3877eccf0d9fb26445089ead8d8b69ef836929735ca15354be68549690e555
-size 52146
+oid sha256:5b1350554c526392f766812801a90d6e18e74b02429263ad7b3e54adcd298a0f
+size 54371
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png
index 7f7a3a82b8d..fd49a2148d9 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_2_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:906b08c68d09d8c5ca41f518928c92239eb90ab30153e827c37f9d0036daca87
-size 43970
+oid sha256:fa5a8d5c71a816f4ab1e2b7170cf261eb28cad216621a5f4104749d8aef6c8bc
+size 46166
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png
index 6ff70baa530..b4e80404ff6 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_3_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aa19b5cd2d27493f0f93820d80649681fda843cc87dab6adf5db6437cc7c1d67
-size 48970
+oid sha256:7425b161321252c543f232c93e411a35cd1a767b02021678455a24b38c759168
+size 50410
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png
index a63acd006b7..8f225b0f658 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_4_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cac29a125e8ea11e65d6fdbe1282c0525f12f8aeaf251fa11103ca6abbe79222
-size 46501
+oid sha256:b4164e30426cd4ea70302c4cb7c0b66bbba87f88780fd5e07fd6a21d184a4ea3
+size 48830
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png
index ec2c3e8463a..26be22d138e 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_5_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:93f885fa860add5822c93522ff3a85c241f236f514007c9e6be4151842c39848
-size 41845
+oid sha256:4afee7f68d5a9ab85842af93654a5cafd7650994124e8b2bd01637b3994500a8
+size 44096
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png
index 8fbc79fcf3d..fb77e99ad9c 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_6_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:989f76fb00e26b75827c9e28887db83d27eaa955a26fde20cc4a900f17cba0d8
-size 46727
+oid sha256:dcc47ba80c3085e81a71fd1111270d5f9995de541d7247bf98929e87ec979bec
+size 49094
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png
index b56d48d004d..614d3a058da 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_7_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6eb5f6d73426f470d55b81dc83f2b3148c57bd358299775910c2a882ae1a1825
-size 42920
+oid sha256:768bf1f4ab4f3c672ccacf874c1e2ec97c527881cda62cf03e9fc7fc00b19fc8
+size 45264
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png
index 8d10d0b0fbb..36778ac3b3d 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_8_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5995b1b0090fcc93144f5305a75b8071e9a77610b9e6d2f2f69a392832386921
-size 45944
+oid sha256:bc98594298bcdf743dbd1a11308ac189ceafedf28b63998f8b10829b770e8ebb
+size 48309
diff --git a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png
index 0fda154e008..ea02c9e4cab 100644
--- a/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png
+++ b/screenshots/de/features.messages.impl.actionlist_ActionListViewContent_Day_9_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2a71b6ebe1727f31bd147963247ed16c25ba18379e8f98c3ca8ef6ba3c119f9b
-size 35606
+oid sha256:3bfd6a17ab1a2ab55ea020afb2a3463801892d0f6532bff1391a57ff1c288148
+size 38050
diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_0_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_0_de.png
index 5fa9d542338..8988c0e9e29 100644
--- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_0_de.png
+++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:432e20dbee2b01faebb0699d11b83dc08f3995b23db622b2645ebdcec1b9cdbc
-size 398406
+oid sha256:9ccd9d75d94f50b0deba10aa5cce3a8053e22f125b02b841f08a5b97f98f4d47
+size 396664
diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png
index 157f8f6272f..c1c8843eec4 100644
--- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png
+++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_1_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:db1ad6cc1ca2d83fd389b11eaf7554d3790ed9710e5af524d7c3dfe73987b0c3
-size 19892
+oid sha256:fe2284acf2a5d906181e44d06cd13d5fb8dae51396f348f8484528e75bf00fd1
+size 54684
diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_2_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_2_de.png
index 4202f8ccbfd..0dba7fd3a3f 100644
--- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_2_de.png
+++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_2_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cd72e9a88a9aa3dcd012657e218a372e2e0078f2748b62df107b0dc9ae56ec75
-size 22245
+oid sha256:694ca900f166ac3a23d87e60020211e8b98c02c16afece44364949ff1c19c149
+size 54650
diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_3_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_3_de.png
index d5f291ce6ae..99a2236795b 100644
--- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_3_de.png
+++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_3_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:13765804d661727d44f40f53d8865183f94651f16d30831ee9ab8b90fd9a4efe
-size 23251
+oid sha256:5101ba438b9f028712fd288160fe6fdec87148962ab02a6d15d02c62fb058945
+size 80736
diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_4_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_4_de.png
deleted file mode 100644
index 0bf11f09756..00000000000
--- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_4_de.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1f7a22a39cd00987efa645df5626eec5e405f08ca046c23d53ce7e506f1654e5
-size 54411
diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_5_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_5_de.png
index 732ac86d779..8988c0e9e29 100644
--- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_5_de.png
+++ b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_5_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b415770af2855daf5cc2a09a38ec955ed5c80f958575ff2f5ddfb053df7d7870
-size 54385
+oid sha256:9ccd9d75d94f50b0deba10aa5cce3a8053e22f125b02b841f08a5b97f98f4d47
+size 396664
diff --git a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_6_de.png b/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_6_de.png
deleted file mode 100644
index f5f28f06a17..00000000000
--- a/screenshots/de/features.messages.impl.attachments.preview_AttachmentsView_6_de.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:ff9a1d9f654b9c2cc5e21ffca1c2e85014f9b0b190b534de0667bc49c05e328f
-size 80690
diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_de.png
index 3caf8ac57fe..435710b5212 100644
--- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_de.png
+++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8ceb7481fba9bc16a2b5aa75915f71100a6136804383854cace129d54871492e
-size 13673
+oid sha256:9da05541fb126929f9e48f4e64db3373979f9535da19f91ef8d9ac911ec9b9f9
+size 13683
diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_de.png
index 60ae5aae0c4..4c366e8e5e5 100644
--- a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_de.png
+++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d4d1d4a86d53c785d75b559a581dfb5191c740e71f781339e25b14ede5d0b3cc
-size 8950
+oid sha256:ed80cafdbc8c53fd8c031818fd4df8d7a63f85cc896d1262301d77bfb0ea9c6f
+size 14415
diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_de.png
new file mode 100644
index 00000000000..225fbedca2b
--- /dev/null
+++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c377c4db1993dc4d81c4726e3cd4d5e50a5ce4e953132f630d5ad3b34c2b34d3
+size 24991
diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_de.png
new file mode 100644
index 00000000000..1793f2cd28f
--- /dev/null
+++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c9fa624de81147b6cc30b329c776fcd1230ab2049283ccf5190cf30bd95b2a29
+size 11154
diff --git a/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_de.png b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_de.png
new file mode 100644
index 00000000000..60ae5aae0c4
--- /dev/null
+++ b/screenshots/de/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d4d1d4a86d53c785d75b559a581dfb5191c740e71f781339e25b14ede5d0b3cc
+size 8950
diff --git a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_de.png b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_de.png
index c577e8c1119..0362d1fd5ab 100644
--- a/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_de.png
+++ b/screenshots/de/features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:32eeac69c1c30c1df6252b38eaed414f2b316fb08e914778e544481ae79ace7f
-size 38115
+oid sha256:882a80aee379aaa550dd33f70d24deca6fb4faf93cc11f830a2080c9279fcb6c
+size 37983
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_0_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_0_de.png
index 55bd273fb73..b3c0d8a1a9c 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_0_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6a3c9133b95879d7f0014a3abd993f3ef0f07ac97795b1bc9a77700867a40104
-size 59909
+oid sha256:ece8c46ed93d567c0c7e083d6b35dcf58d515c418f1348bc1a596ae5cd0e415e
+size 57824
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png
index f0e7376bf1e..144d57de771 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_10_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2a1a3f69c036c334179a9fb219d12a12825b7a9c40f38854583ec98ec3ca9f4b
-size 59939
+oid sha256:95b86ce0454c63decc5283ce07a81f76773e3312eca6a27ef0026959041d30fc
+size 57852
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png
index b62aa5fe3e4..e741b958589 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_11_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:944a96ffbc396029275d6a20bd2227c310a7262da774503bd3a52bc4d762647a
-size 63273
+oid sha256:9d186fcc63bb524c23b6b42e6c4fab6f902271c755fc9c7de21ff609b2ab6483
+size 61170
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_1_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_1_de.png
index 714c0567926..d40b5db1ad7 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_1_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_1_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:156369e5f7e94f58f825f2249f201a5d7b40655f1425b774a17445e518fcf161
-size 59093
+oid sha256:6ccc4aa5c095c7c01585d66f5caf146eba7060ba361ec03719c8474abb6f5ccf
+size 57002
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_4_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_4_de.png
index 6ee127e60f9..a430ccafa87 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_4_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_4_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:18244f7e05810813387b5b7452b83f2da6d3ffc90795315a598a14cc684e18ca
-size 56125
+oid sha256:a7320a1c07fdef11132084db9a5d0c33daf96e2a62cce7c687dc2342bf5f7544
+size 55422
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_5_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_5_de.png
index 28d2d44b523..66ed2c1d1c3 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_5_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_5_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5d8c86158553769b70234e06c359002dcff225e0d0106f0d1bd39c6850aee7ad
-size 57724
+oid sha256:da56456bff1a3c3227dac7fd307ea2e65b6ed06d18198321d23db71e282370c8
+size 55629
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_7_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_7_de.png
index c19d6aca204..8805b4d2195 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_7_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_7_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2c99fc5403f85c8c124825d89199c57e66ba83fdb2d3a240ba3aa5c371dfd39d
-size 60006
+oid sha256:af12ee2fff1baacc2035bb2fcc91280801d3e00e47eef153f5a2d8d420aeef5e
+size 59278
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_8_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_8_de.png
index 4a5a3a5696e..e6d07e3ba6b 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_8_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_8_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:80100385d90b8b40d6b65307cb2dc365e1650c4957c0fd62bf22bda5a6a84165
-size 62388
+oid sha256:76d16e02ae85372018b0e4a75640344fcf6ef6ccfe628a3d0df3d39cd0cad0fe
+size 60292
diff --git a/screenshots/de/features.messages.impl_MessagesView_Day_9_de.png b/screenshots/de/features.messages.impl_MessagesView_Day_9_de.png
index b3b07021b11..e2b84ff5cb8 100644
--- a/screenshots/de/features.messages.impl_MessagesView_Day_9_de.png
+++ b/screenshots/de/features.messages.impl_MessagesView_Day_9_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cea15456754678c29eda54cd40b67d6a979baf5f244839119d107d9135ac5f84
-size 48428
+oid sha256:2f52f75694f30f122948890b47aed021f724862b9e13f8c33a722e0e85c89446
+size 47720
diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_0_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_0_de.png
index 427e5ca89f8..735b7b68346 100644
--- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_0_de.png
+++ b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a1ce9cdff91b1779c8012cbb5a3d97fa3d3a2f234137504e649401a55f18bb79
-size 315251
+oid sha256:832fe4b9ed7b53c161374cc72438c49f254569b33d64ded5f661152976b0d883
+size 315128
diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_1_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_1_de.png
index f633bfa019b..441a167459f 100644
--- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_1_de.png
+++ b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_1_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:139052189bb9fd0925057ca6f24c153e2b74f2152bd96176cbf748acdc32657a
-size 310709
+oid sha256:a5bb6a1bec8b521c06651f31825f2245441232cd89c4da53280ddc29c3554f91
+size 310728
diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_2_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_2_de.png
index 473d54b9297..b77eaee61d0 100644
--- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_2_de.png
+++ b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_2_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eb3ef7da385418584ceae0bf4f8f2b66f2342b5f924118976807a89a974d69d0
-size 313709
+oid sha256:d9dff767d267c198d1cdcdc79c313b7fd04d1d90436dd754fc0e07b69a2dddc5
+size 313727
diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_3_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_3_de.png
index 45cc6093435..42ac955c15c 100644
--- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_3_de.png
+++ b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_3_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5cf4084319b5af180e18fe7ef56be0f02e28007da76b2a575300d40c809d9d22
-size 308069
+oid sha256:fead83f04b6fa70a2f3b1cd9c05e99a37b5ab8d8a6fab2c0d3d665c6d0838613
+size 307936
diff --git a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_4_de.png b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_4_de.png
index 9dc1afd109c..d94ddc8e5b2 100644
--- a/screenshots/de/features.onboarding.impl_OnBoardingView_Day_4_de.png
+++ b/screenshots/de/features.onboarding.impl_OnBoardingView_Day_4_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:286c322d0b255ff738fbd8f1985582e8e7d9d88e7918054bf2fd47e184490b60
-size 316029
+oid sha256:7b6fd1e8ef08f97c918548e19e63a6419f2142aedd587a39877f53d5ea56f14f
+size 315910
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png
index d8024b6d6b2..8871186de7b 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0cdb41e5b33d52884986643249d7a42e93636a4e93b632138f4d3f774ddbca96
-size 44333
+oid sha256:d425759ce4b53ac6d22c6d7e4d7247475c27c8215f9921dacc6841a53a9d1142
+size 45124
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png
index c7f71897e30..b3545619f27 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_10_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fd9287cf3ac81b3b4a6540680fc300de0eff1b340cbb266f777f08da95771061
-size 46477
+oid sha256:8f75f0bebda631a579af2e2134c6607c7c4f17bb754103efed4635a018e01a9c
+size 43055
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png
index 416513ad1da..5dedf203738 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_11_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6c9d5236b9d015df5c1b7d552bf8581e08b323c6fda64486f317f0d128d2dd8a
-size 44965
+oid sha256:cefb929363fd99af735a72dbabaea5739070d0650a9d8246a8a5d5692e3048f5
+size 41528
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png
index 620f6be4cd9..c9950f17fb5 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_12_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8f7700caa63e98fda360e728203e9335e854b91c2b7f81216c07797d3c934c52
-size 48711
+oid sha256:8ce4ddfc7fc5631cb8e29fef0b16304945880eb168ab189d7f39b32e256a66fe
+size 45310
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png
index 741fa7213e0..4759f565faf 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_13_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:03095ef49ba0cbde8b47c9a847dd289b4c1a24e172462525916ae11a210ee40f
-size 46947
+oid sha256:492b29d4ef8c74cf034b69bd275e3b3f347dbbaa2721da562333931e88ae32c0
+size 43536
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png
index 208bdd75f09..6e76284a392 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_1_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b1b09c9d1731e44795d1e1bf223e5cf36a788871da45db0fff15926b232ae386
-size 42841
+oid sha256:0b858541821efc8d2af82d187f690104835d6f160f54a4e2ccda679365fc2631
+size 45065
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png
index 7ba1f6f69af..9fe459ab48d 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_2_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cb4b0c46733c0a85e2b8bb8988c65d1921beac559059db298bb68a53eaf6e916
-size 43872
+oid sha256:ab0f0595b3e5d938385fa7deca3c8659d61a341609d3d4abb4c1710a14dba9f8
+size 41639
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png
index 93378213f25..8c705242a47 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_3_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:90c3d0bcc4c19fd9bde289a9ecf1be62f138a100bb778566834e586b1f851514
-size 41954
+oid sha256:498fbafd36c7937ad4dee2aae3ff62dad5308e9e4d652cfcc752382e643d5c05
+size 43963
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png
index aa2e03e1529..9ff5b22ea1c 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_4_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3c005f4f95aefeee9ba0c377ed9fdbdfddfe08de46c84b39ed5040fc7ce150ff
-size 47300
+oid sha256:ad50323615fac194d9b9f4eedc5af23c6bbf4e93115f9c248e2c2d8576f2e548
+size 41655
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png
index 7304223f0ba..9fa130f77cb 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_5_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:961b01e9819ec6cfe9cb9a2f9d886f31522fc074f242ba46d68425c58d2e9fc4
-size 44652
+oid sha256:cf47366437b0584743b500130c053492c8a5949a2460b970f05d4c159dd85e4d
+size 40602
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png
index 770f470ecaf..b6629b1245d 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_6_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:44b04195c62acb148b2141aca28b196da98d16d69d157c74799ce6448244069b
-size 42321
+oid sha256:0122d333057a6bd11a8656a829b4802763ce4888c7162cb59a748cc4983f1249
+size 44306
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png
index 7e512a91ddf..5ba62eefa69 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_7_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:509c49c39b91b08574895a3eef883d144be39875eb8b73e2dc1a134f800aaa6b
-size 47996
+oid sha256:29d478297b24260081d2d8cfd6f0c831dd10b9034898ad51d4ef20e00e5650f1
+size 44589
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png
index 86791411653..9cb36c56133 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_8_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:74c22cae39733ed87399930e280a0b0e8349214c421cd1b4dd3ca8c627701026
-size 46847
+oid sha256:9f3b9914340665a33055e3ad3818d95c19f02494ca0dc595ad5609418c8fa351
+size 43433
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png
index 748fb71ce66..c97daa7de0e 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetailsDark_9_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5a9c496e14d257d6a795746e63a49a51e38acd681da5536a802ead4a07e10d93
-size 45841
+oid sha256:1bb4a925033aefb5040fcc86b955de736103ec01cc1843de7a866f4a67fc3c26
+size 42412
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png
index 4b10e12a57a..34aa4b52aa4 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:25756397b0b0c680f2dec68cabe3fd3ab4143e3bbb51c6a5a74fc9e4d4a1eb1d
-size 45462
+oid sha256:82f1b321eebce911ed8a370ba7157dca53d88ff9ddcfe67d5e0e9cd6fe537c9b
+size 46322
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png
index 63928bced5d..2a00f842668 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_10_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cc88d4575e116f6140be3dda7671ab31f7cb01361599b316a35abc181cd712b3
-size 47694
+oid sha256:cab57b99da9930b37a85d1f47ccaba5ead9f3f766b69195e21d24840f7cf7801
+size 44146
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png
index 96e46836ac3..d5a95d0d2fe 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_11_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8a7bd3b8e62ee11c28e60b843f02e0eb979c1542162dad0c744875261bf0351d
-size 46149
+oid sha256:ac06406bf6ccb21d213f54703c86f56912868e22c53f3b25512cfa1e5857ab4d
+size 42578
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png
index e0a94ffc9ed..d042837947c 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_12_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8bc966615b5fdfa6e175e9f8719d2165432979650da63e49b266a2b4c6476676
-size 49509
+oid sha256:11d76f61037c90160b3a8907c4658a953864a3f70b1dee01d51ead2fba2b8304
+size 45989
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png
index 30905e62341..6068dbf315a 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_13_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:54b8b26f2e613f47ff8d61e9d95cf0c8276d581e055318087612d65e2d6b3906
-size 48172
+oid sha256:b768e0eed672cd9f44e893e8dffaf65172b53a326c82ef92d0bf91e6dc3b590a
+size 44623
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png
index 72918f3603f..8b4b53ce1c4 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_1_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:23ed6de2bdb340fbe1cc1df828c1e9aab94a6fc4dc2ecc065e62ea6221fd8c6a
-size 44072
+oid sha256:4e0c782561e7908ccf850999535a033e5d5335e9bf803914ff5628e73d967e3f
+size 46464
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png
index 0dfe364e107..9f5b0398580 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_2_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3d8e3b3e577734855df04202b71d14b67305b6ac2ee94ad76463bf566f2ce0b3
-size 45163
+oid sha256:71a92f09bbc7154847e18913efb257832f8c923e16d22cbe2f5192da8b79f3c2
+size 42980
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png
index 8642af7f91b..37f890e3a94 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_3_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c4b4cd38658310e85e9ddf998d11ba71dc6f86f283b12d7f17ccfd0e8c9e2a8c
-size 42936
+oid sha256:4d757b721581c9af6f514fe77706f5754b55fadc1f1741745e790ce106cb80eb
+size 45046
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png
index ab024d6a8a3..f2e0cbba03d 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_4_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e2a6b836c92ec82cca629e3a6eedb88ff54a4d6835fe2273bec70b4976552b47
-size 48531
+oid sha256:a0f33ff72cf22c76a287240b478199b22fe9aac47396afc9977420aa6635ce3e
+size 42663
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png
index 6f64e91347e..49d25686a40 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_5_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ba73053e9cae0bd22110a56105b6ccb9e7a031c2d0f2426713b524ae204589ed
-size 45844
+oid sha256:90c91b006f581e53ca7dad338541ca11e54c28e5f727a762724bfb8b55af336e
+size 41574
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png
index c74aa90e566..86b61a41d89 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_6_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:97e2145171681c40026f49f2c36d5375b679c6b01482d83aaa433bbbe95a5bbe
-size 43594
+oid sha256:084e692b87ce3e1bca84b4b523472b008470d1186943cfb9edd517bce203fee2
+size 45628
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png
index 7120d62d2b9..90a268259da 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_7_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ee8d7ab50be5c2b2b7edf00dffea3049719c3da73446e2e357c9b9ef73542129
-size 49312
+oid sha256:75a2dfa1a15c2934aff43378aae990f1bcb31fff5c0fdfae2e422f7a8c1c6657
+size 45787
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png
index f531ecc171a..43c14085186 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_8_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a21b5087be13fc6a3eb484e61b3b6a7f82a998db5a0637211aa6182bb4c5e130
-size 48121
+oid sha256:70ca007b72b292a47043ff15aa5b993c58debd875ce18fb27838f8e741771a75
+size 44579
diff --git a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png
index 1291a4b1f42..2a007ada50c 100644
--- a/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png
+++ b/screenshots/de/features.roomdetails.impl_RoomDetails_9_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2c9ff7395ff777f0ef5589c284da266d1bd5507c237661023a8b392b03bf4210
-size 47017
+oid sha256:9e592ea37d4e5a434bb485ee3a1cfc447cb30e087dd6f59190f82c76a3f26d67
+size 43467
diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_0_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_0_de.png
new file mode 100644
index 00000000000..acd5520e3f0
--- /dev/null
+++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_0_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f68e4700488f385b2f087d6146dc824a8c8658d0e319ed885824758253b68b3
+size 98799
diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_1_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_1_de.png
new file mode 100644
index 00000000000..eff9f40e2c3
--- /dev/null
+++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_1_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:872452fc3d6cf65c479fcbcdceaad74454abf4edb6955207526a72f1284076dd
+size 83442
diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_2_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_2_de.png
new file mode 100644
index 00000000000..57fdb849106
--- /dev/null
+++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_2_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d765335160d7fd0037ff32c5c98dcd0bcabb73e019e5c45be738359a90bc6890
+size 87846
diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_3_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_3_de.png
new file mode 100644
index 00000000000..c22f29397c9
--- /dev/null
+++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_3_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9fc51b69b66dfdb1c0a5ae3608b054897cdbbc6ee4652c62b99707ccd2b6183
+size 78073
diff --git a/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_4_de.png b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_4_de.png
new file mode 100644
index 00000000000..d9940453cb5
--- /dev/null
+++ b/screenshots/de/libraries.dateformatter.impl.previews_DateFormatterModeView_4_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:47e7e518ab182ebc202089f4d0c00bb9bc66c792c98d5ac9d3c304a49149eebc
+size 75659
diff --git a/screenshots/de/libraries.mediaviewer.api.viewer_MediaViewerView_2_de.png b/screenshots/de/libraries.mediaviewer.api.viewer_MediaViewerView_2_de.png
deleted file mode 100644
index e5a2758a48e..00000000000
--- a/screenshots/de/libraries.mediaviewer.api.viewer_MediaViewerView_2_de.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:2e19bc448799c94926aa3ad247ad4da2ec5b1c40166dce4f3c17679faa426b73
-size 71611
diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png
new file mode 100644
index 00000000000..82873fa1afa
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f99c07228385b0f6e55c56c646313f71704527bdb6fd0bd6db1e3de2023c1453
+size 31736
diff --git a/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png
new file mode 100644
index 00000000000..58e74b79a29
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da37a12f69987fb0da647a554b19003d8848dcb031b47ec6fc805ac1c3545bcb
+size 37871
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_de.png
new file mode 100644
index 00000000000..70de94855cf
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b11021336c578ecac2aa14aa6eb46e4e90f8e5d669a93972f03d310d7ca4117d
+size 17960
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_de.png
new file mode 100644
index 00000000000..f85206c5a41
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:11caaa5e8211c15870ec43394e5caee37c53aa2c6694ff80a3abe168e12d95fe
+size 15233
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_de.png
new file mode 100644
index 00000000000..70de94855cf
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b11021336c578ecac2aa14aa6eb46e4e90f8e5d669a93972f03d310d7ca4117d
+size 17960
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_de.png
new file mode 100644
index 00000000000..78189ef5a71
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:76835141c32686a88d2079f6fd29e13f4138e96d65a787c8e2bc3d09efe8003b
+size 31487
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_de.png
new file mode 100644
index 00000000000..2e4cf5001b3
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ddea5e17f11507e89971ae989ccf086be5f96befb1ca415f906353547739294
+size 18915
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_de.png
new file mode 100644
index 00000000000..10b88c4bb98
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:16f1fe0273366dfef31dbe47130f15ba8ba815c3bcd9b3bd1082dc2fd6a10f09
+size 18076
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_de.png
new file mode 100644
index 00000000000..10b88c4bb98
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:16f1fe0273366dfef31dbe47130f15ba8ba815c3bcd9b3bd1082dc2fd6a10f09
+size 18076
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_de.png
new file mode 100644
index 00000000000..ae2864d0d4b
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:66bd49d3b004205daf1e85398c1b879137f7a88367355c079e3fe9ee942ffe2a
+size 29252
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_de.png
new file mode 100644
index 00000000000..3baca8477d1
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:274279303004121b7716cc292272ff7164e772ad6949c322d2e34843f633f1d6
+size 41516
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_de.png
new file mode 100644
index 00000000000..14bf073dd59
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1ba2a2125d1e3bf3f7bf4209714ab90b09319e2a6fd4cf3689ae489f1c9e49dd
+size 44970
diff --git a/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_de.png b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_de.png
new file mode 100644
index 00000000000..7f5b267ac5f
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:77e765f8752cf596ba76ed5d4a351968ed272069b1c508c041ab32d1b61d5289
+size 15165
diff --git a/screenshots/de/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Day_0_de.png b/screenshots/de/libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_de.png
similarity index 100%
rename from screenshots/de/libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Day_0_de.png
rename to screenshots/de/libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_de.png
diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_11_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_11_de.png
new file mode 100644
index 00000000000..3d3b373fddb
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_11_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d202f768dd0df5f48579764515ff73dd43ef5f61697e2127b4d22d23c514bf7c
+size 38644
diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_12_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_12_de.png
new file mode 100644
index 00000000000..3c2bb54f73d
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_12_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1f472dc0688242ec7d398b69ab8933019c740ee2a717dc503b1098e1f7d10474
+size 32802
diff --git a/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_2_de.png b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_2_de.png
new file mode 100644
index 00000000000..74531259301
--- /dev/null
+++ b/screenshots/de/libraries.mediaviewer.impl.viewer_MediaViewerView_2_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e016fcaccf00e8bb0e09b9e9fd0243769b5096a3fec4300a0fcaafce0236f941
+size 72153
diff --git a/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png b/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png
new file mode 100644
index 00000000000..034b838c950
--- /dev/null
+++ b/screenshots/de/libraries.textcomposer_CaptionWarningBottomSheet_Day_0_de.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:581bc19df178842dc2168c5a4a5b68e252064dfa2ac464b33669f64d931f0ce3
+size 21056
diff --git a/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png b/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png
index d341017e17c..630a153541f 100644
--- a/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png
+++ b/screenshots/de/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b6809dcd5a339af0adf428e6aa198ca1719bee480e2cc7130a2cfd0feb88e06e
-size 62949
+oid sha256:42488fd254e1f3af4daa2dbc89d474f6fc33ff32e65fe3a5f1c225fcf440b4f8
+size 55344
diff --git a/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png
index eb9d617febb..a7adc543f64 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerAddCaption_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:72d90402ff38639d384be74db19bc8ee5e8dfc15def5e24839c404809e74829c
-size 65689
+oid sha256:e21e91e0d2fa375a1228f56aca68ec6db1209e5c49970d88d8e72a83362308e2
+size 61266
diff --git a/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png
index 16aabac42df..aa645ae51bf 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerCaption_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:70681c983dd8027ef21a7cf7f1ce21b2aa49608026a2f265d4ea2b452f394cc0
-size 57592
+oid sha256:077d64f7179ad1d32ff633c5e4074270095d9579508b9a396c05437774810a41
+size 48936
diff --git a/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png
index aaf06e57ab9..4217c75857d 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerEditCaption_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ed323016c3aa4c61d248f7f515c4008bea91b5ab07513f1ad85c448b28a8e071
-size 66415
+oid sha256:51f950b5fbb386756f920bcffe055eac581edfa78f566ff857dacc41761a03df
+size 61347
diff --git a/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png
index d341017e17c..630a153541f 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerEdit_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b6809dcd5a339af0adf428e6aa198ca1719bee480e2cc7130a2cfd0feb88e06e
-size 62949
+oid sha256:42488fd254e1f3af4daa2dbc89d474f6fc33ff32e65fe3a5f1c225fcf440b4f8
+size 55344
diff --git a/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png
index 5c10dd38bc3..972c016e3e1 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerFormatting_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:875f73e545458cc85207df61fbf1373f88aa862853ab21cfd6fb351bb91948be
-size 59545
+oid sha256:d992143ff08f81f2b2bce2128e8c6ad7520ad98953a3114b44565ec2f43e2d95
+size 52946
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png
index 51f5f5770a3..9081a2f732f 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ee02ad92f874446ebd74e46502fd45614fc97c0034675a4447dce351798d8be6
-size 82047
+oid sha256:6b6804e4c9415ba75da5ca9b16be07c6b1eda1b6d84e2d563ef692cb93617d32
+size 75820
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png
index 7b5d6c9bcdb..65b9db12be0 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_10_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2b5861f697ef912bb02b8419dbdf7243510e1942a3f716a60c63a6aecd79a3b9
-size 65337
+oid sha256:3faaf28767463aa8fe5961a61c6fdf992c7d618b19733bc91150471a88020cb6
+size 58864
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png
index ef37f98bd82..b6c1f2e50eb 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_11_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e4b9230dda20f3871bb6253731bf0dc04b92e09a6441851962323ccac68a6caa
-size 80092
+oid sha256:84100fcdc5b70f67f9b2165bdf8eb77cccc8f05cd61b0bdcd8ba9039d7949449
+size 73845
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png
index 0456b59845d..a53d365ccd2 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_1_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a24829845f382f53961346b127e567110209eda399edeb0e3bc87e184a8fe048
-size 91429
+oid sha256:2d6aa5d6f29e38888aaaf9539f66bd3235c9ea8a9096e48a6538086057b0b473
+size 85309
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png
index ef07526a306..4b664b00557 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_2_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7d1fd1eddd14d06b2b8624b213fa38fab109097a5cd718449973547d6a6f49e8
-size 68618
+oid sha256:9b853ffe20e9d4c8c10c86caec538eb302b496d7c18232ac0325a498085cc9e4
+size 62069
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png
index ee40a061ca3..16864c1179d 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_3_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0997a54e0fdf6c321d6012b5e9f9825f2d5f4cdc060e9ea866e8c65e7d04927a
-size 66870
+oid sha256:dba9bd70b11b1747e945aa12ccbb94b40c3025cf70efaf961eb44da4e75be8b8
+size 60340
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png
index a3179a0f592..86b1424d2df 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_4_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:acc8dfa9e4c996dec342558fa40ac6b2f26b6d8f9a218aacc3a8fbe1a49fca80
-size 74825
+oid sha256:bc3d3b82f386467567eefa83522d21c6d93c0561f5e5cc6301cbacf521686802
+size 68386
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png
index 0ed418ba839..72f44ed719d 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_5_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:df1f9c9a662bcf58e4e3699abadaacf4aef0b0039fbcae5ce1cf050d5d6a344b
-size 65775
+oid sha256:afe16af06bfedd3b030f3a7d27e8d8f63e89068625c3530c71f50a627fcc6a07
+size 59291
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png
index a54b4c686d6..d1114778105 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_6_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e26de5d2a495c686f7214b0ff62f277482d272ba36c60055d545001ff620614c
-size 66628
+oid sha256:93eeb7258381e4e5026226a6226da75975fd3db36d28c57359df0fc8f832bfc4
+size 60072
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png
index 64da7bda0dc..58bd7612b5d 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_7_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3036d2cc2517b22c2101e4a836be340c19f8b318f63d1bc7e2e75385d9bff2ce
-size 68819
+oid sha256:635fe16b8cf1bf574c989fbf2b0cd302674b66c101ddeb2cde3f2dcb7426ea00
+size 62282
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png
index 0c1e0f279b3..d55b3812f39 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_8_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6c1cf0d780d9027d6b4d1aa3408b3e3c2df5e33e43ecce13c1f710f2773d6532
-size 76468
+oid sha256:c00782f67da13a617718e50dbb6b21e6e4ded8ee29ac1c6e2eb6ae8374050b78
+size 70410
diff --git a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png
index 855c2cb40d4..76a546a60c1 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerReply_Day_9_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e0a312aae8b862b08675ad66b18288fe4f509e98b0767715c4b0ab706e1b995f
-size 66070
+oid sha256:1906f2a814ab6ddb0f6ca509f2eb28b45515285f6db2acb7822c2c72d49b5884
+size 59543
diff --git a/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png b/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png
index fc57718382c..491ce0d9f00 100644
--- a/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png
+++ b/screenshots/de/libraries.textcomposer_TextComposerSimple_Day_0_de.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bdee2ff1500639827661775d87822dd7262019fea3709dad0e8c503e0c43b5a7
-size 51421
+oid sha256:7fe8f3ac71d021e8799b9536bcc81e8570d742d768f3b07fc87bc9f31f17f0e7
+size 46244
diff --git a/screenshots/html/data.js b/screenshots/html/data.js
index ca26a3463f9..f32931e67ea 100644
--- a/screenshots/html/data.js
+++ b/screenshots/html/data.js
@@ -1,59 +1,59 @@
// Generated file, do not edit
export const screenshots = [
["en","en-dark","de",],
-["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20056,],
+["features.preferences.impl.about_AboutView_Day_0_en","features.preferences.impl.about_AboutView_Night_0_en",20070,],
["features.invite.impl.response_AcceptDeclineInviteView_Day_0_en","features.invite.impl.response_AcceptDeclineInviteView_Night_0_en",0,],
-["features.invite.impl.response_AcceptDeclineInviteView_Day_1_en","features.invite.impl.response_AcceptDeclineInviteView_Night_1_en",20056,],
-["features.invite.impl.response_AcceptDeclineInviteView_Day_2_en","features.invite.impl.response_AcceptDeclineInviteView_Night_2_en",20056,],
-["features.invite.impl.response_AcceptDeclineInviteView_Day_3_en","features.invite.impl.response_AcceptDeclineInviteView_Night_3_en",20056,],
-["features.invite.impl.response_AcceptDeclineInviteView_Day_4_en","features.invite.impl.response_AcceptDeclineInviteView_Night_4_en",20056,],
-["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20056,],
-["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20059,],
-["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20056,],
-["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20056,],
-["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20056,],
+["features.invite.impl.response_AcceptDeclineInviteView_Day_1_en","features.invite.impl.response_AcceptDeclineInviteView_Night_1_en",20070,],
+["features.invite.impl.response_AcceptDeclineInviteView_Day_2_en","features.invite.impl.response_AcceptDeclineInviteView_Night_2_en",20070,],
+["features.invite.impl.response_AcceptDeclineInviteView_Day_3_en","features.invite.impl.response_AcceptDeclineInviteView_Night_3_en",20070,],
+["features.invite.impl.response_AcceptDeclineInviteView_Day_4_en","features.invite.impl.response_AcceptDeclineInviteView_Night_4_en",20070,],
+["features.logout.impl_AccountDeactivationView_Day_0_en","features.logout.impl_AccountDeactivationView_Night_0_en",20070,],
+["features.logout.impl_AccountDeactivationView_Day_1_en","features.logout.impl_AccountDeactivationView_Night_1_en",20070,],
+["features.logout.impl_AccountDeactivationView_Day_2_en","features.logout.impl_AccountDeactivationView_Night_2_en",20070,],
+["features.logout.impl_AccountDeactivationView_Day_3_en","features.logout.impl_AccountDeactivationView_Night_3_en",20070,],
+["features.logout.impl_AccountDeactivationView_Day_4_en","features.logout.impl_AccountDeactivationView_Night_4_en",20070,],
["features.login.impl.accountprovider_AccountProviderView_Day_0_en","features.login.impl.accountprovider_AccountProviderView_Night_0_en",0,],
["features.login.impl.accountprovider_AccountProviderView_Day_1_en","features.login.impl.accountprovider_AccountProviderView_Night_1_en",0,],
["features.login.impl.accountprovider_AccountProviderView_Day_2_en","features.login.impl.accountprovider_AccountProviderView_Night_2_en",0,],
["features.login.impl.accountprovider_AccountProviderView_Day_3_en","features.login.impl.accountprovider_AccountProviderView_Night_3_en",0,],
["features.messages.impl.actionlist_ActionListViewContent_Day_0_en","features.messages.impl.actionlist_ActionListViewContent_Night_0_en",0,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20056,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_10_en","features.messages.impl.actionlist_ActionListViewContent_Night_10_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_11_en","features.messages.impl.actionlist_ActionListViewContent_Night_11_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_12_en","features.messages.impl.actionlist_ActionListViewContent_Night_12_en",20070,],
["features.messages.impl.actionlist_ActionListViewContent_Day_1_en","features.messages.impl.actionlist_ActionListViewContent_Night_1_en",0,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20056,],
-["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20056,],
-["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20056,],
-["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20056,],
-["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20056,],
-["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20056,],
-["features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en",20056,],
-["features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en",20056,],
-["features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en",20056,],
-["features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en",20056,],
-["features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en",20056,],
-["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20056,],
-["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20056,],
-["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20056,],
-["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20056,],
-["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20056,],
-["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",20056,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_2_en","features.messages.impl.actionlist_ActionListViewContent_Night_2_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_3_en","features.messages.impl.actionlist_ActionListViewContent_Night_3_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_4_en","features.messages.impl.actionlist_ActionListViewContent_Night_4_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_5_en","features.messages.impl.actionlist_ActionListViewContent_Night_5_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_6_en","features.messages.impl.actionlist_ActionListViewContent_Night_6_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_7_en","features.messages.impl.actionlist_ActionListViewContent_Night_7_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_8_en","features.messages.impl.actionlist_ActionListViewContent_Night_8_en",20070,],
+["features.messages.impl.actionlist_ActionListViewContent_Day_9_en","features.messages.impl.actionlist_ActionListViewContent_Night_9_en",20070,],
+["features.createroom.impl.addpeople_AddPeopleView_Day_0_en","features.createroom.impl.addpeople_AddPeopleView_Night_0_en",20070,],
+["features.createroom.impl.addpeople_AddPeopleView_Day_1_en","features.createroom.impl.addpeople_AddPeopleView_Night_1_en",20070,],
+["features.createroom.impl.addpeople_AddPeopleView_Day_2_en","features.createroom.impl.addpeople_AddPeopleView_Night_2_en",20070,],
+["features.createroom.impl.addpeople_AddPeopleView_Day_3_en","features.createroom.impl.addpeople_AddPeopleView_Night_3_en",20070,],
+["features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en",20070,],
+["features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en",20070,],
+["features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en",20070,],
+["features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en",20070,],
+["features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en","features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en",20070,],
+["libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en","",20070,],
+["libraries.designsystem.components.dialogs_AlertDialog_Day_0_en","libraries.designsystem.components.dialogs_AlertDialog_Night_0_en",20070,],
+["features.analytics.impl_AnalyticsOptInView_Day_0_en","features.analytics.impl_AnalyticsOptInView_Night_0_en",20070,],
+["features.analytics.api.preferences_AnalyticsPreferencesView_Day_0_en","features.analytics.api.preferences_AnalyticsPreferencesView_Night_0_en",20070,],
+["features.preferences.impl.analytics_AnalyticsSettingsView_Day_0_en","features.preferences.impl.analytics_AnalyticsSettingsView_Night_0_en",20070,],
+["services.apperror.impl_AppErrorView_Day_0_en","services.apperror.impl_AppErrorView_Night_0_en",20070,],
["libraries.designsystem.components.async_AsyncActionView_Day_0_en","libraries.designsystem.components.async_AsyncActionView_Night_0_en",0,],
-["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20056,],
+["libraries.designsystem.components.async_AsyncActionView_Day_1_en","libraries.designsystem.components.async_AsyncActionView_Night_1_en",20070,],
["libraries.designsystem.components.async_AsyncActionView_Day_2_en","libraries.designsystem.components.async_AsyncActionView_Night_2_en",0,],
-["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20056,],
+["libraries.designsystem.components.async_AsyncActionView_Day_3_en","libraries.designsystem.components.async_AsyncActionView_Night_3_en",20070,],
["libraries.designsystem.components.async_AsyncActionView_Day_4_en","libraries.designsystem.components.async_AsyncActionView_Night_4_en",0,],
-["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20056,],
+["libraries.designsystem.components.async_AsyncFailure_Day_0_en","libraries.designsystem.components.async_AsyncFailure_Night_0_en",20070,],
["libraries.designsystem.components.async_AsyncIndicatorFailure_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorFailure_Night_0_en",0,],
["libraries.designsystem.components.async_AsyncIndicatorLoading_Day_0_en","libraries.designsystem.components.async_AsyncIndicatorLoading_Night_0_en",0,],
["libraries.designsystem.components.async_AsyncLoading_Day_0_en","libraries.designsystem.components.async_AsyncLoading_Night_0_en",0,],
-["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20056,],
+["features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Day_0_en","features.messages.impl.messagecomposer_AttachmentSourcePickerMenu_Night_0_en",20070,],
["libraries.matrix.ui.components_AttachmentThumbnail_Day_0_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_0_en",0,],
["libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en",0,],
["libraries.matrix.ui.components_AttachmentThumbnail_Day_2_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_2_en",0,],
@@ -63,15 +63,17 @@ export const screenshots = [
["libraries.matrix.ui.components_AttachmentThumbnail_Day_6_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_6_en",0,],
["libraries.matrix.ui.components_AttachmentThumbnail_Day_7_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_7_en",0,],
["libraries.matrix.ui.components_AttachmentThumbnail_Day_8_en","libraries.matrix.ui.components_AttachmentThumbnail_Night_8_en",0,],
-["features.messages.impl.attachments.preview_AttachmentsView_0_en","",20059,],
-["features.messages.impl.attachments.preview_AttachmentsView_1_en","",20059,],
-["features.messages.impl.attachments.preview_AttachmentsView_2_en","",20059,],
-["features.messages.impl.attachments.preview_AttachmentsView_3_en","",20059,],
-["features.messages.impl.attachments.preview_AttachmentsView_4_en","",20056,],
-["features.messages.impl.attachments.preview_AttachmentsView_5_en","",20056,],
-["features.messages.impl.attachments.preview_AttachmentsView_6_en","",20059,],
-["features.messages.impl.attachments.preview_AttachmentsView_7_en","",0,],
-["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20056,],
+["features.messages.impl.attachments.preview_AttachmentsView_0_en","",20070,],
+["features.messages.impl.attachments.preview_AttachmentsView_1_en","",20070,],
+["features.messages.impl.attachments.preview_AttachmentsView_2_en","",20070,],
+["features.messages.impl.attachments.preview_AttachmentsView_3_en","",20070,],
+["features.messages.impl.attachments.preview_AttachmentsView_4_en","",0,],
+["features.messages.impl.attachments.preview_AttachmentsView_5_en","",20070,],
+["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_3_en","libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_3_en",0,],
+["libraries.matrix.ui.components_AvatarActionBottomSheet_Day_0_en","libraries.matrix.ui.components_AvatarActionBottomSheet_Night_0_en",20070,],
["libraries.designsystem.components.avatar_Avatar_Avatars_0_en","",0,],
["libraries.designsystem.components.avatar_Avatar_Avatars_10_en","",0,],
["libraries.designsystem.components.avatar_Avatar_Avatars_11_en","",0,],
@@ -147,20 +149,29 @@ export const screenshots = [
["libraries.designsystem.components.avatar_Avatar_Avatars_75_en","",0,],
["libraries.designsystem.components.avatar_Avatar_Avatars_76_en","",0,],
["libraries.designsystem.components.avatar_Avatar_Avatars_77_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_78_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_79_en","",0,],
["libraries.designsystem.components.avatar_Avatar_Avatars_7_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_80_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_81_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_82_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_83_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_84_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_85_en","",0,],
+["libraries.designsystem.components.avatar_Avatar_Avatars_86_en","",0,],
["libraries.designsystem.components.avatar_Avatar_Avatars_8_en","",0,],
["libraries.designsystem.components.avatar_Avatar_Avatars_9_en","",0,],
["libraries.designsystem.components.button_BackButton_Buttons_en","",0,],
["libraries.designsystem.components_Badge_Day_0_en","libraries.designsystem.components_Badge_Night_0_en",0,],
["libraries.designsystem.components_BigCheckmark_Day_0_en","libraries.designsystem.components_BigCheckmark_Night_0_en",0,],
["libraries.designsystem.components_BigIcon_Day_0_en","libraries.designsystem.components_BigIcon_Night_0_en",0,],
-["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20056,],
-["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20056,],
-["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20056,],
-["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20056,],
-["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20056,],
-["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20056,],
-["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20056,],
+["features.preferences.impl.blockedusers_BlockedUsersView_Day_0_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_0_en",20070,],
+["features.preferences.impl.blockedusers_BlockedUsersView_Day_1_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_1_en",20070,],
+["features.preferences.impl.blockedusers_BlockedUsersView_Day_2_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_2_en",20070,],
+["features.preferences.impl.blockedusers_BlockedUsersView_Day_3_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_3_en",20070,],
+["features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en",20070,],
+["features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en",20070,],
+["features.preferences.impl.blockedusers_BlockedUsersView_Day_6_en","features.preferences.impl.blockedusers_BlockedUsersView_Night_6_en",20070,],
["libraries.designsystem.components_BloomInitials_Day_0_en","libraries.designsystem.components_BloomInitials_Night_0_en",0,],
["libraries.designsystem.components_BloomInitials_Day_1_en","libraries.designsystem.components_BloomInitials_Night_1_en",0,],
["libraries.designsystem.components_BloomInitials_Day_2_en","libraries.designsystem.components_BloomInitials_Night_2_en",0,],
@@ -171,115 +182,123 @@ export const screenshots = [
["libraries.designsystem.components_BloomInitials_Day_7_en","libraries.designsystem.components_BloomInitials_Night_7_en",0,],
["libraries.designsystem.components_Bloom_Day_0_en","libraries.designsystem.components_Bloom_Night_0_en",0,],
["libraries.designsystem.theme.components_BottomSheetDragHandle_Day_0_en","libraries.designsystem.theme.components_BottomSheetDragHandle_Night_0_en",0,],
-["features.rageshake.impl.bugreport_BugReportView_Day_0_en","features.rageshake.impl.bugreport_BugReportView_Night_0_en",20056,],
-["features.rageshake.impl.bugreport_BugReportView_Day_1_en","features.rageshake.impl.bugreport_BugReportView_Night_1_en",20056,],
-["features.rageshake.impl.bugreport_BugReportView_Day_2_en","features.rageshake.impl.bugreport_BugReportView_Night_2_en",20056,],
-["features.rageshake.impl.bugreport_BugReportView_Day_3_en","features.rageshake.impl.bugreport_BugReportView_Night_3_en",20056,],
-["features.rageshake.impl.bugreport_BugReportView_Day_4_en","features.rageshake.impl.bugreport_BugReportView_Night_4_en",20056,],
+["features.rageshake.impl.bugreport_BugReportView_Day_0_en","features.rageshake.impl.bugreport_BugReportView_Night_0_en",20070,],
+["features.rageshake.impl.bugreport_BugReportView_Day_1_en","features.rageshake.impl.bugreport_BugReportView_Night_1_en",20070,],
+["features.rageshake.impl.bugreport_BugReportView_Day_2_en","features.rageshake.impl.bugreport_BugReportView_Night_2_en",20070,],
+["features.rageshake.impl.bugreport_BugReportView_Day_3_en","features.rageshake.impl.bugreport_BugReportView_Night_3_en",20070,],
+["features.rageshake.impl.bugreport_BugReportView_Day_4_en","features.rageshake.impl.bugreport_BugReportView_Night_4_en",20070,],
["libraries.designsystem.atomic.molecules_ButtonColumnMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ButtonColumnMolecule_Night_0_en",0,],
["libraries.designsystem.atomic.molecules_ButtonRowMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ButtonRowMolecule_Night_0_en",0,],
["features.messages.impl.timeline.components_CallMenuItem_Day_0_en","features.messages.impl.timeline.components_CallMenuItem_Night_0_en",0,],
["features.messages.impl.timeline.components_CallMenuItem_Day_1_en","features.messages.impl.timeline.components_CallMenuItem_Night_1_en",0,],
-["features.messages.impl.timeline.components_CallMenuItem_Day_2_en","features.messages.impl.timeline.components_CallMenuItem_Night_2_en",20056,],
-["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20056,],
+["features.messages.impl.timeline.components_CallMenuItem_Day_2_en","features.messages.impl.timeline.components_CallMenuItem_Night_2_en",20070,],
+["features.messages.impl.timeline.components_CallMenuItem_Day_3_en","features.messages.impl.timeline.components_CallMenuItem_Night_3_en",20070,],
["features.messages.impl.timeline.components_CallMenuItem_Day_4_en","features.messages.impl.timeline.components_CallMenuItem_Night_4_en",0,],
["features.call.impl.ui_CallScreenPipView_Day_0_en","features.call.impl.ui_CallScreenPipView_Night_0_en",0,],
["features.call.impl.ui_CallScreenPipView_Day_1_en","features.call.impl.ui_CallScreenPipView_Night_1_en",0,],
["features.call.impl.ui_CallScreenView_Day_0_en","features.call.impl.ui_CallScreenView_Night_0_en",0,],
-["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20056,],
-["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20056,],
-["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20056,],
-["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en",20056,],
+["features.call.impl.ui_CallScreenView_Day_1_en","features.call.impl.ui_CallScreenView_Night_1_en",20070,],
+["features.call.impl.ui_CallScreenView_Day_2_en","features.call.impl.ui_CallScreenView_Night_2_en",20070,],
+["features.call.impl.ui_CallScreenView_Day_3_en","features.call.impl.ui_CallScreenView_Night_3_en",20070,],
+["libraries.textcomposer_CaptionWarningBottomSheet_Day_0_en","libraries.textcomposer_CaptionWarningBottomSheet_Night_0_en",20070,],
+["features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en","features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_0_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_10_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_10_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_1_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_1_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_2_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_2_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_3_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_3_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_4_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_4_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_5_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_5_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_6_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_6_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_7_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_7_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_8_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_8_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Day_9_en","features.roomdetails.impl.rolesandpermissions.changeroles_ChangeRolesView_Night_9_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_0_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_1_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_2_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_3_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_4_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_5_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions.permissions_ChangeRoomPermissionsView_Night_6_en",20070,],
["features.login.impl.changeserver_ChangeServerView_Day_0_en","features.login.impl.changeserver_ChangeServerView_Night_0_en",0,],
-["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20056,],
-["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20056,],
+["features.login.impl.changeserver_ChangeServerView_Day_1_en","features.login.impl.changeserver_ChangeServerView_Night_1_en",20070,],
+["features.login.impl.changeserver_ChangeServerView_Day_2_en","features.login.impl.changeserver_ChangeServerView_Night_2_en",20070,],
["libraries.matrix.ui.components_CheckableResolvedUserRow_en","",0,],
-["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20056,],
+["libraries.matrix.ui.components_CheckableUnresolvedUserRow_en","",20070,],
["libraries.designsystem.theme.components_Checkboxes_Toggles_en","",0,],
["libraries.designsystem.theme.components_CircularProgressIndicator_Progress_Indicators_en","",0,],
["libraries.designsystem.components_ClickableLinkText_Text_en","",0,],
["libraries.designsystem.theme_ColorAliases_Day_0_en","libraries.designsystem.theme_ColorAliases_Night_0_en",0,],
-["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20056,],
-["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20056,],
-["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20056,],
+["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_0_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_0_en",20070,],
+["libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Day_1_en","libraries.designsystem.atomic.molecules_ComposerAlertMolecule_Night_1_en",20070,],
+["libraries.textcomposer_ComposerModeView_Day_0_en","libraries.textcomposer_ComposerModeView_Night_0_en",20070,],
["libraries.textcomposer_ComposerModeView_Day_1_en","libraries.textcomposer_ComposerModeView_Night_1_en",0,],
["libraries.textcomposer_ComposerModeView_Day_2_en","libraries.textcomposer_ComposerModeView_Night_2_en",0,],
["libraries.textcomposer_ComposerModeView_Day_3_en","libraries.textcomposer_ComposerModeView_Night_3_en",0,],
["libraries.textcomposer.components_ComposerOptionsButton_Day_0_en","libraries.textcomposer.components_ComposerOptionsButton_Night_0_en",0,],
["libraries.designsystem.components.avatar_CompositeAvatar_Avatars_en","",0,],
-["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20056,],
-["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20056,],
+["features.createroom.impl.configureroom_ConfigureRoomViewDark_0_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewDark_1_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewDark_2_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewDark_3_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewDark_4_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewDark_5_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewLight_0_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewLight_1_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewLight_2_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewLight_3_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewLight_4_en","",20070,],
+["features.createroom.impl.configureroom_ConfigureRoomViewLight_5_en","",20070,],
["features.preferences.impl.developer.tracing_ConfigureTracingView_Day_0_en","features.preferences.impl.developer.tracing_ConfigureTracingView_Night_0_en",0,],
-["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20056,],
-["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20056,],
-["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20056,],
-["features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20056,],
+["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en",20070,],
+["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en",20070,],
+["features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en","features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en",20070,],
+["features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_ConfirmRecoveryKeyBanner_Night_0_en",20070,],
["libraries.designsystem.components.dialogs_ConfirmationDialogContent_Dialogs_en","",0,],
["libraries.designsystem.components.dialogs_ConfirmationDialog_Day_0_en","libraries.designsystem.components.dialogs_ConfirmationDialog_Night_0_en",0,],
["features.networkmonitor.api.ui_ConnectivityIndicatorView_Day_0_en","features.networkmonitor.api.ui_ConnectivityIndicatorView_Night_0_en",0,],
-["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20056,],
-["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20056,],
-["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20056,],
-["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20056,],
-["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20056,],
-["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20056,],
-["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20056,],
-["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20056,],
-["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20056,],
-["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20056,],
-["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20056,],
-["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20056,],
-["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20056,],
-["features.createroom.impl.root_CreateRoomRootView_Day_0_en","features.createroom.impl.root_CreateRoomRootView_Night_0_en",20056,],
-["features.createroom.impl.root_CreateRoomRootView_Day_1_en","features.createroom.impl.root_CreateRoomRootView_Night_1_en",20056,],
-["features.createroom.impl.root_CreateRoomRootView_Day_2_en","features.createroom.impl.root_CreateRoomRootView_Night_2_en",20056,],
-["features.createroom.impl.root_CreateRoomRootView_Day_3_en","features.createroom.impl.root_CreateRoomRootView_Night_3_en",20056,],
-["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20056,],
-["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20056,],
+["features.rageshake.api.crash_CrashDetectionView_Day_0_en","features.rageshake.api.crash_CrashDetectionView_Night_0_en",20070,],
+["features.login.impl.screens.createaccount_CreateAccountView_Day_0_en","features.login.impl.screens.createaccount_CreateAccountView_Night_0_en",20070,],
+["features.login.impl.screens.createaccount_CreateAccountView_Day_1_en","features.login.impl.screens.createaccount_CreateAccountView_Night_1_en",20070,],
+["features.login.impl.screens.createaccount_CreateAccountView_Day_2_en","features.login.impl.screens.createaccount_CreateAccountView_Night_2_en",20070,],
+["features.login.impl.screens.createaccount_CreateAccountView_Day_3_en","features.login.impl.screens.createaccount_CreateAccountView_Night_3_en",20070,],
+["features.poll.impl.create_CreatePollView_Day_0_en","features.poll.impl.create_CreatePollView_Night_0_en",20070,],
+["features.poll.impl.create_CreatePollView_Day_1_en","features.poll.impl.create_CreatePollView_Night_1_en",20070,],
+["features.poll.impl.create_CreatePollView_Day_2_en","features.poll.impl.create_CreatePollView_Night_2_en",20070,],
+["features.poll.impl.create_CreatePollView_Day_3_en","features.poll.impl.create_CreatePollView_Night_3_en",20070,],
+["features.poll.impl.create_CreatePollView_Day_4_en","features.poll.impl.create_CreatePollView_Night_4_en",20070,],
+["features.poll.impl.create_CreatePollView_Day_5_en","features.poll.impl.create_CreatePollView_Night_5_en",20070,],
+["features.poll.impl.create_CreatePollView_Day_6_en","features.poll.impl.create_CreatePollView_Night_6_en",20070,],
+["features.poll.impl.create_CreatePollView_Day_7_en","features.poll.impl.create_CreatePollView_Night_7_en",20070,],
+["features.createroom.impl.root_CreateRoomRootView_Day_0_en","features.createroom.impl.root_CreateRoomRootView_Night_0_en",20070,],
+["features.createroom.impl.root_CreateRoomRootView_Day_1_en","features.createroom.impl.root_CreateRoomRootView_Night_1_en",20070,],
+["features.createroom.impl.root_CreateRoomRootView_Day_2_en","features.createroom.impl.root_CreateRoomRootView_Night_2_en",20070,],
+["features.createroom.impl.root_CreateRoomRootView_Day_3_en","features.createroom.impl.root_CreateRoomRootView_Night_3_en",20070,],
+["libraries.dateformatter.impl.previews_DateFormatterModeView_0_en","",20073,],
+["libraries.dateformatter.impl.previews_DateFormatterModeView_1_en","",20073,],
+["libraries.dateformatter.impl.previews_DateFormatterModeView_2_en","",20073,],
+["libraries.dateformatter.impl.previews_DateFormatterModeView_3_en","",20073,],
+["libraries.dateformatter.impl.previews_DateFormatterModeView_4_en","",20073,],
+["libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_0_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_1_en",0,],
+["libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en","",20070,],
+["libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en","",20070,],
["features.logout.impl.direct_DefaultDirectLogoutView_Day_0_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_0_en",0,],
-["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20056,],
-["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20056,],
-["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20056,],
+["features.logout.impl.direct_DefaultDirectLogoutView_Day_1_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_1_en",20070,],
+["features.logout.impl.direct_DefaultDirectLogoutView_Day_2_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_2_en",20070,],
+["features.logout.impl.direct_DefaultDirectLogoutView_Day_3_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_3_en",20070,],
["features.logout.impl.direct_DefaultDirectLogoutView_Day_4_en","features.logout.impl.direct_DefaultDirectLogoutView_Night_4_en",0,],
-["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20056,],
-["features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en",20056,],
-["features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBar_Night_0_en",20056,],
+["features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Day_0_en","features.preferences.impl.notifications.edit_DefaultNotificationSettingOption_Night_0_en",20070,],
+["features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBarWithIndicator_Night_0_en",20070,],
+["features.roomlist.impl.components_DefaultRoomListTopBar_Day_0_en","features.roomlist.impl.components_DefaultRoomListTopBar_Night_0_en",20070,],
["features.licenses.impl.details_DependenciesDetailsView_Day_0_en","features.licenses.impl.details_DependenciesDetailsView_Night_0_en",0,],
-["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20056,],
-["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20056,],
-["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20056,],
-["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20056,],
-["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20056,],
-["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20056,],
-["libraries.designsystem.atomic.molecules_DialogLikeBannerMolecule_Day_0_en","libraries.designsystem.atomic.molecules_DialogLikeBannerMolecule_Night_0_en",20056,],
+["features.licenses.impl.list_DependencyLicensesListView_Day_0_en","features.licenses.impl.list_DependencyLicensesListView_Night_0_en",20070,],
+["features.licenses.impl.list_DependencyLicensesListView_Day_1_en","features.licenses.impl.list_DependencyLicensesListView_Night_1_en",20070,],
+["features.licenses.impl.list_DependencyLicensesListView_Day_2_en","features.licenses.impl.list_DependencyLicensesListView_Night_2_en",20070,],
+["features.preferences.impl.developer_DeveloperSettingsView_Day_0_en","features.preferences.impl.developer_DeveloperSettingsView_Night_0_en",20070,],
+["features.preferences.impl.developer_DeveloperSettingsView_Day_1_en","features.preferences.impl.developer_DeveloperSettingsView_Night_1_en",20070,],
+["features.preferences.impl.developer_DeveloperSettingsView_Day_2_en","features.preferences.impl.developer_DeveloperSettingsView_Night_2_en",20070,],
+["libraries.designsystem.atomic.molecules_DialogLikeBannerMolecule_Day_0_en","libraries.designsystem.atomic.molecules_DialogLikeBannerMolecule_Night_0_en",20070,],
["libraries.designsystem.theme.components_DialogWithDestructiveButton_Dialog_with_destructive_button_Dialogs_en","",0,],
["libraries.designsystem.theme.components_DialogWithOnlyMessageAndOkButton_Dialog_with_only_message_and_ok_button_Dialogs_en","",0,],
["libraries.designsystem.theme.components_DialogWithThirdButton_Dialog_with_third_button_Dialogs_en","",0,],
@@ -291,12 +310,12 @@ export const screenshots = [
["libraries.designsystem.text_DpScale_1_0f__en","",0,],
["libraries.designsystem.text_DpScale_1_5f__en","",0,],
["libraries.designsystem.theme.components_DropdownMenuItem_Menus_en","",0,],
-["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20056,],
-["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20056,],
-["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20056,],
-["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20056,],
-["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20056,],
-["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20056,],
+["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_0_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_0_en",20070,],
+["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_1_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_1_en",20070,],
+["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_2_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_2_en",20070,],
+["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_3_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_3_en",20070,],
+["features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Day_4_en","features.preferences.impl.notifications.edit_EditDefaultNotificationSettingView_Night_4_en",20070,],
+["features.preferences.impl.user.editprofile_EditUserProfileView_Day_0_en","features.preferences.impl.user.editprofile_EditUserProfileView_Night_0_en",20070,],
["libraries.matrix.ui.components_EditableAvatarView_Day_0_en","libraries.matrix.ui.components_EditableAvatarView_Night_0_en",0,],
["libraries.matrix.ui.components_EditableAvatarView_Day_1_en","libraries.matrix.ui.components_EditableAvatarView_Night_1_en",0,],
["libraries.matrix.ui.components_EditableAvatarView_Day_2_en","libraries.matrix.ui.components_EditableAvatarView_Night_2_en",0,],
@@ -306,11 +325,14 @@ export const screenshots = [
["libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Day_0_en","libraries.designsystem.atomic.atoms_ElementLogoAtomMedium_Night_0_en",0,],
["features.messages.impl.timeline.components.customreaction_EmojiItem_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiItem_Night_0_en",0,],
["features.messages.impl.timeline.components.customreaction_EmojiPicker_Day_0_en","features.messages.impl.timeline.components.customreaction_EmojiPicker_Night_0_en",0,],
-["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20056,],
-["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20056,],
-["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20056,],
+["libraries.designsystem.components.dialogs_ErrorDialogContent_Dialogs_en","",20070,],
+["libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialogWithDoNotShowAgain_Night_0_en",20070,],
+["libraries.designsystem.components.dialogs_ErrorDialog_Day_0_en","libraries.designsystem.components.dialogs_ErrorDialog_Night_0_en",20070,],
["features.messages.impl.timeline.debug_EventDebugInfoView_Day_0_en","features.messages.impl.timeline.debug_EventDebugInfoView_Night_0_en",0,],
["libraries.featureflag.ui_FeatureListView_Day_0_en","libraries.featureflag.ui_FeatureListView_Night_0_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en","libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en",0,],
["libraries.designsystem.theme.components_FilledButtonLargeLowPadding_Buttons_en","",0,],
["libraries.designsystem.theme.components_FilledButtonLarge_Buttons_en","",0,],
["libraries.designsystem.theme.components_FilledButtonMediumLowPadding_Buttons_en","",0,],
@@ -323,15 +345,15 @@ export const screenshots = [
["libraries.designsystem.theme.components_FloatingActionButton_Floating_Action_Buttons_en","",0,],
["libraries.designsystem.atomic.pages_FlowStepPage_Day_0_en","libraries.designsystem.atomic.pages_FlowStepPage_Night_0_en",0,],
["features.messages.impl.timeline.focus_FocusRequestStateView_Day_0_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_0_en",0,],
-["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20056,],
-["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20056,],
-["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20056,],
+["features.messages.impl.timeline.focus_FocusRequestStateView_Day_1_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_1_en",20070,],
+["features.messages.impl.timeline.focus_FocusRequestStateView_Day_2_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_2_en",20070,],
+["features.messages.impl.timeline.focus_FocusRequestStateView_Day_3_en","features.messages.impl.timeline.focus_FocusRequestStateView_Night_3_en",20070,],
["libraries.textcomposer.components_FormattingOption_Day_0_en","libraries.textcomposer.components_FormattingOption_Night_0_en",0,],
["features.messages.impl.forward_ForwardMessagesView_Day_0_en","features.messages.impl.forward_ForwardMessagesView_Night_0_en",0,],
["features.messages.impl.forward_ForwardMessagesView_Day_1_en","features.messages.impl.forward_ForwardMessagesView_Night_1_en",0,],
["features.messages.impl.forward_ForwardMessagesView_Day_2_en","features.messages.impl.forward_ForwardMessagesView_Night_2_en",0,],
-["features.messages.impl.forward_ForwardMessagesView_Day_3_en","features.messages.impl.forward_ForwardMessagesView_Night_3_en",20056,],
-["features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.roomlist.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20056,],
+["features.messages.impl.forward_ForwardMessagesView_Day_3_en","features.messages.impl.forward_ForwardMessagesView_Night_3_en",20070,],
+["features.roomlist.impl.components_FullScreenIntentPermissionBanner_Day_0_en","features.roomlist.impl.components_FullScreenIntentPermissionBanner_Night_0_en",20070,],
["libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en",0,],
["libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en","libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en",0,],
["features.messages.impl.timeline.components.group_GroupHeaderView_Day_0_en","features.messages.impl.timeline.components.group_GroupHeaderView_Night_0_en",0,],
@@ -343,8 +365,8 @@ export const screenshots = [
["libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitlePlaceholdersRowMolecule_Night_0_en",0,],
["libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Day_0_en","libraries.designsystem.atomic.molecules_IconTitleSubtitleMolecule_Night_0_en",0,],
["libraries.designsystem.theme.components_IconToggleButton_Toggles_en","",0,],
-["appicon.element_Icon_en","",0,],
["appicon.enterprise_Icon_en","",0,],
+["appicon.element_Icon_en","",0,],
["libraries.designsystem.icons_IconsCompound_Day_0_en","libraries.designsystem.icons_IconsCompound_Night_0_en",0,],
["libraries.designsystem.icons_IconsCompound_Day_1_en","libraries.designsystem.icons_IconsCompound_Night_1_en",0,],
["libraries.designsystem.icons_IconsCompound_Day_2_en","libraries.designsystem.icons_IconsCompound_Night_2_en",0,],
@@ -353,53 +375,72 @@ export const screenshots = [
["libraries.designsystem.icons_IconsCompound_Day_5_en","libraries.designsystem.icons_IconsCompound_Night_5_en",0,],
["libraries.designsystem.icons_IconsOther_Day_0_en","libraries.designsystem.icons_IconsOther_Night_0_en",0,],
["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_0_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_0_en",0,],
-["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20056,],
-["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20056,],
+["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_1_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_1_en",20070,],
+["features.messages.impl.crypto.identity_IdentityChangeStateView_Day_2_en","features.messages.impl.crypto.identity_IdentityChangeStateView_Night_2_en",20070,],
+["libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en",0,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_0_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_0_en",0,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_10_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_10_en",0,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_11_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_11_en",0,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_1_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_1_en",0,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_2_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_2_en",0,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_3_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_3_en",0,],
-["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20056,],
+["libraries.matrix.ui.messages.reply_InReplyToView_Day_4_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_4_en",20070,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en",0,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_6_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_6_en",0,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_7_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_7_en",0,],
-["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20056,],
+["libraries.matrix.ui.messages.reply_InReplyToView_Day_8_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_8_en",20070,],
["libraries.matrix.ui.messages.reply_InReplyToView_Day_9_en","libraries.matrix.ui.messages.reply_InReplyToView_Night_9_en",0,],
-["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20056,],
-["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20059,],
-["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20056,],
-["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20056,],
-["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20056,],
-["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20056,],
-["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20056,],
-["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20056,],
-["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20056,],
+["features.call.impl.ui_IncomingCallScreen_Day_0_en","features.call.impl.ui_IncomingCallScreen_Night_0_en",20070,],
+["features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en",20070,],
+["features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en",20070,],
+["features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en",20070,],
+["features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en",20070,],
+["features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en",20070,],
+["features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en",20070,],
+["features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en",20070,],
+["features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en","features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en",20070,],
["libraries.designsystem.atomic.molecules_InfoListItemMolecule_Day_0_en","libraries.designsystem.atomic.molecules_InfoListItemMolecule_Night_0_en",0,],
["libraries.designsystem.atomic.organisms_InfoListOrganism_Day_0_en","libraries.designsystem.atomic.organisms_InfoListOrganism_Night_0_en",0,],
-["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20056,],
+["libraries.matrix.ui.components_InviteSenderView_Day_0_en","libraries.matrix.ui.components_InviteSenderView_Night_0_en",20070,],
["features.joinroom.impl_JoinRoomView_Day_0_en","features.joinroom.impl_JoinRoomView_Night_0_en",0,],
-["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20059,],
-["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20059,],
-["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20059,],
-["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20056,],
-["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20056,],
-["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20056,],
-["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20056,],
-["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20056,],
-["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20056,],
-["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20056,],
-["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20056,],
+["features.joinroom.impl_JoinRoomView_Day_10_en","features.joinroom.impl_JoinRoomView_Night_10_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_11_en","features.joinroom.impl_JoinRoomView_Night_11_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_12_en","features.joinroom.impl_JoinRoomView_Night_12_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_1_en","features.joinroom.impl_JoinRoomView_Night_1_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_2_en","features.joinroom.impl_JoinRoomView_Night_2_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_3_en","features.joinroom.impl_JoinRoomView_Night_3_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_4_en","features.joinroom.impl_JoinRoomView_Night_4_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_5_en","features.joinroom.impl_JoinRoomView_Night_5_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_6_en","features.joinroom.impl_JoinRoomView_Night_6_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_7_en","features.joinroom.impl_JoinRoomView_Night_7_en",20070,],
+["features.joinroom.impl_JoinRoomView_Day_8_en","features.joinroom.impl_JoinRoomView_Night_8_en",20070,],
["features.joinroom.impl_JoinRoomView_Day_9_en","features.joinroom.impl_JoinRoomView_Night_9_en",0,],
+["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en",20073,],
+["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en",20073,],
+["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en",20073,],
+["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en",20073,],
+["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en",20073,],
+["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en",20073,],
+["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en",20073,],
+["features.knockrequests.impl.banner_KnockRequestsBannerView_Day_7_en","features.knockrequests.impl.banner_KnockRequestsBannerView_Night_7_en",20073,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_0_en","features.knockrequests.impl.list_KnockRequestsListView_Night_0_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_1_en","features.knockrequests.impl.list_KnockRequestsListView_Night_1_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_2_en","features.knockrequests.impl.list_KnockRequestsListView_Night_2_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_3_en","features.knockrequests.impl.list_KnockRequestsListView_Night_3_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_4_en","features.knockrequests.impl.list_KnockRequestsListView_Night_4_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_5_en","features.knockrequests.impl.list_KnockRequestsListView_Night_5_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_6_en","features.knockrequests.impl.list_KnockRequestsListView_Night_6_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_7_en","features.knockrequests.impl.list_KnockRequestsListView_Night_7_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_8_en","features.knockrequests.impl.list_KnockRequestsListView_Night_8_en",20070,],
+["features.knockrequests.impl.list_KnockRequestsListView_Day_9_en","features.knockrequests.impl.list_KnockRequestsListView_Night_9_en",20070,],
["libraries.designsystem.components_LabelledCheckbox_Toggles_en","",0,],
["features.leaveroom.api_LeaveRoomView_Day_0_en","features.leaveroom.api_LeaveRoomView_Night_0_en",0,],
-["features.leaveroom.api_LeaveRoomView_Day_1_en","features.leaveroom.api_LeaveRoomView_Night_1_en",20056,],
-["features.leaveroom.api_LeaveRoomView_Day_2_en","features.leaveroom.api_LeaveRoomView_Night_2_en",20056,],
-["features.leaveroom.api_LeaveRoomView_Day_3_en","features.leaveroom.api_LeaveRoomView_Night_3_en",20056,],
-["features.leaveroom.api_LeaveRoomView_Day_4_en","features.leaveroom.api_LeaveRoomView_Night_4_en",20056,],
-["features.leaveroom.api_LeaveRoomView_Day_5_en","features.leaveroom.api_LeaveRoomView_Night_5_en",20056,],
-["features.leaveroom.api_LeaveRoomView_Day_6_en","features.leaveroom.api_LeaveRoomView_Night_6_en",20056,],
+["features.leaveroom.api_LeaveRoomView_Day_1_en","features.leaveroom.api_LeaveRoomView_Night_1_en",20070,],
+["features.leaveroom.api_LeaveRoomView_Day_2_en","features.leaveroom.api_LeaveRoomView_Night_2_en",20070,],
+["features.leaveroom.api_LeaveRoomView_Day_3_en","features.leaveroom.api_LeaveRoomView_Night_3_en",20070,],
+["features.leaveroom.api_LeaveRoomView_Day_4_en","features.leaveroom.api_LeaveRoomView_Night_4_en",20070,],
+["features.leaveroom.api_LeaveRoomView_Day_5_en","features.leaveroom.api_LeaveRoomView_Night_5_en",20070,],
+["features.leaveroom.api_LeaveRoomView_Day_6_en","features.leaveroom.api_LeaveRoomView_Night_6_en",20070,],
["libraries.designsystem.background_LightGradientBackground_Day_0_en","libraries.designsystem.background_LightGradientBackground_Night_0_en",0,],
["libraries.designsystem.theme.components_LinearProgressIndicator_Progress_Indicators_en","",0,],
["libraries.designsystem.components.dialogs_ListDialogContent_Dialogs_en","",0,],
@@ -456,29 +497,29 @@ export const screenshots = [
["libraries.designsystem.theme.components_ListSupportingTextSmallPadding_List_supporting_text_-_small_padding_List_sections_en","",0,],
["libraries.textcomposer.components_LiveWaveformView_Day_0_en","libraries.textcomposer.components_LiveWaveformView_Night_0_en",0,],
["appnav.room.joined_LoadingRoomNodeView_Day_0_en","appnav.room.joined_LoadingRoomNodeView_Night_0_en",0,],
-["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20056,],
-["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20056,],
-["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20056,],
-["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20056,],
+["appnav.room.joined_LoadingRoomNodeView_Day_1_en","appnav.room.joined_LoadingRoomNodeView_Night_1_en",20070,],
+["features.lockscreen.impl.settings_LockScreenSettingsView_Day_0_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_0_en",20070,],
+["features.lockscreen.impl.settings_LockScreenSettingsView_Day_1_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_1_en",20070,],
+["features.lockscreen.impl.settings_LockScreenSettingsView_Day_2_en","features.lockscreen.impl.settings_LockScreenSettingsView_Night_2_en",20070,],
["appnav.loggedin_LoggedInView_Day_0_en","appnav.loggedin_LoggedInView_Night_0_en",0,],
-["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20056,],
-["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20056,],
-["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20056,],
-["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20056,],
-["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20056,],
-["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20056,],
-["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20056,],
-["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20056,],
-["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20056,],
-["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20056,],
-["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20056,],
-["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20056,],
-["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20056,],
-["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20056,],
-["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20056,],
-["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20056,],
+["appnav.loggedin_LoggedInView_Day_1_en","appnav.loggedin_LoggedInView_Night_1_en",20070,],
+["appnav.loggedin_LoggedInView_Day_2_en","appnav.loggedin_LoggedInView_Night_2_en",20070,],
+["appnav.loggedin_LoggedInView_Day_3_en","appnav.loggedin_LoggedInView_Night_3_en",20070,],
+["features.login.impl.screens.loginpassword_LoginPasswordView_Day_0_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_0_en",20070,],
+["features.login.impl.screens.loginpassword_LoginPasswordView_Day_1_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_1_en",20070,],
+["features.login.impl.screens.loginpassword_LoginPasswordView_Day_2_en","features.login.impl.screens.loginpassword_LoginPasswordView_Night_2_en",20070,],
+["features.logout.impl_LogoutView_Day_0_en","features.logout.impl_LogoutView_Night_0_en",20070,],
+["features.logout.impl_LogoutView_Day_1_en","features.logout.impl_LogoutView_Night_1_en",20070,],
+["features.logout.impl_LogoutView_Day_2_en","features.logout.impl_LogoutView_Night_2_en",20070,],
+["features.logout.impl_LogoutView_Day_3_en","features.logout.impl_LogoutView_Night_3_en",20070,],
+["features.logout.impl_LogoutView_Day_4_en","features.logout.impl_LogoutView_Night_4_en",20070,],
+["features.logout.impl_LogoutView_Day_5_en","features.logout.impl_LogoutView_Night_5_en",20070,],
+["features.logout.impl_LogoutView_Day_6_en","features.logout.impl_LogoutView_Night_6_en",20070,],
+["features.logout.impl_LogoutView_Day_7_en","features.logout.impl_LogoutView_Night_7_en",20070,],
+["features.logout.impl_LogoutView_Day_8_en","features.logout.impl_LogoutView_Night_8_en",20070,],
+["features.logout.impl_LogoutView_Day_9_en","features.logout.impl_LogoutView_Night_9_en",20070,],
["libraries.designsystem.components.button_MainActionButton_Buttons_en","",0,],
-["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20056,],
+["libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en","libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en",20070,],
["libraries.textcomposer.components.markdown_MarkdownTextInput_Day_0_en","libraries.textcomposer.components.markdown_MarkdownTextInput_Night_0_en",0,],
["libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomNegative_Night_0_en",0,],
["libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Day_0_en","libraries.designsystem.atomic.atoms_MatrixBadgeAtomNeutral_Night_0_en",0,],
@@ -488,24 +529,46 @@ export const screenshots = [
["libraries.matrix.ui.components_MatrixUserHeader_Day_1_en","libraries.matrix.ui.components_MatrixUserHeader_Night_1_en",0,],
["libraries.matrix.ui.components_MatrixUserRow_Day_0_en","libraries.matrix.ui.components_MatrixUserRow_Night_0_en",0,],
["libraries.matrix.ui.components_MatrixUserRow_Day_1_en","libraries.matrix.ui.components_MatrixUserRow_Night_1_en",0,],
-["libraries.mediaviewer.api.player_MediaPlayerControllerView_Day_0_en","libraries.mediaviewer.api.player_MediaPlayerControllerView_Night_0_en",0,],
-["libraries.mediaviewer.api.player_MediaPlayerControllerView_Day_1_en","libraries.mediaviewer.api.player_MediaPlayerControllerView_Night_1_en",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_0_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_10_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_1_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_2_en","",20056,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_3_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_4_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_5_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_6_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_7_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_8_en","",0,],
-["libraries.mediaviewer.api.viewer_MediaViewerView_9_en","",0,],
+["libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en","libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en",0,],
+["libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en","libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en",0,],
+["libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en",20073,],
+["libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en","libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en",20073,],
+["libraries.mediaviewer.impl.local.file_MediaFileView_Day_0_en","libraries.mediaviewer.impl.local.file_MediaFileView_Night_0_en",0,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en",20073,],
+["libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en","libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en",20073,],
+["libraries.mediaviewer.impl.local.image_MediaImageView_Day_0_en","libraries.mediaviewer.impl.local.image_MediaImageView_Night_0_en",0,],
+["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en",0,],
+["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en",0,],
+["libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en","libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en",0,],
+["libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en","libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_0_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_10_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_11_en","",20073,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_12_en","",20073,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_13_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_1_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_2_en","",20070,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_3_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_4_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_5_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_6_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_7_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_8_en","",0,],
+["libraries.mediaviewer.impl.viewer_MediaViewerView_9_en","",0,],
["libraries.designsystem.theme.components_MediumTopAppBar_App_Bars_en","",0,],
["libraries.textcomposer.mentions_MentionSpanTheme_Day_0_en","libraries.textcomposer.mentions_MentionSpanTheme_Night_0_en",0,],
["libraries.designsystem.theme.components.previews_Menu_Menus_en","",0,],
["features.messages.impl.messagecomposer_MessageComposerViewVoice_Day_0_en","features.messages.impl.messagecomposer_MessageComposerViewVoice_Night_0_en",0,],
-["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20056,],
+["features.messages.impl.messagecomposer_MessageComposerView_Day_0_en","features.messages.impl.messagecomposer_MessageComposerView_Night_0_en",20070,],
["features.messages.impl.timeline.components_MessageEventBubble_Day_0_en","features.messages.impl.timeline.components_MessageEventBubble_Night_0_en",0,],
["features.messages.impl.timeline.components_MessageEventBubble_Day_10_en","features.messages.impl.timeline.components_MessageEventBubble_Night_10_en",0,],
["features.messages.impl.timeline.components_MessageEventBubble_Day_11_en","features.messages.impl.timeline.components_MessageEventBubble_Night_11_en",0,],
@@ -522,7 +585,7 @@ export const screenshots = [
["features.messages.impl.timeline.components_MessageEventBubble_Day_7_en","features.messages.impl.timeline.components_MessageEventBubble_Night_7_en",0,],
["features.messages.impl.timeline.components_MessageEventBubble_Day_8_en","features.messages.impl.timeline.components_MessageEventBubble_Night_8_en",0,],
["features.messages.impl.timeline.components_MessageEventBubble_Day_9_en","features.messages.impl.timeline.components_MessageEventBubble_Night_9_en",0,],
-["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20056,],
+["features.messages.impl.timeline.components_MessageShieldView_Day_0_en","features.messages.impl.timeline.components_MessageShieldView_Night_0_en",20070,],
["features.messages.impl.timeline.components_MessageStateEventContainer_Day_0_en","features.messages.impl.timeline.components_MessageStateEventContainer_Night_0_en",0,],
["features.messages.impl.timeline.components_MessagesReactionButtonAdd_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonAdd_Night_0_en",0,],
["features.messages.impl.timeline.components_MessagesReactionButtonExtra_Day_0_en","features.messages.impl.timeline.components_MessagesReactionButtonExtra_Night_0_en",0,],
@@ -530,23 +593,23 @@ export const screenshots = [
["features.messages.impl.timeline.components_MessagesReactionButton_Day_1_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_1_en",0,],
["features.messages.impl.timeline.components_MessagesReactionButton_Day_2_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_2_en",0,],
["features.messages.impl.timeline.components_MessagesReactionButton_Day_3_en","features.messages.impl.timeline.components_MessagesReactionButton_Night_3_en",0,],
-["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20056,],
-["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20056,],
-["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20056,],
-["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20056,],
-["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20056,],
-["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20056,],
-["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20056,],
-["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20056,],
-["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20056,],
-["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20056,],
-["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20056,],
-["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20056,],
-["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20056,],
-["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20056,],
-["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20056,],
+["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en",20070,],
+["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en",20070,],
+["features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en","features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en",20070,],
+["features.messages.impl_MessagesView_Day_0_en","features.messages.impl_MessagesView_Night_0_en",20070,],
+["features.messages.impl_MessagesView_Day_10_en","features.messages.impl_MessagesView_Night_10_en",20070,],
+["features.messages.impl_MessagesView_Day_11_en","features.messages.impl_MessagesView_Night_11_en",20070,],
+["features.messages.impl_MessagesView_Day_1_en","features.messages.impl_MessagesView_Night_1_en",20070,],
+["features.messages.impl_MessagesView_Day_2_en","features.messages.impl_MessagesView_Night_2_en",20070,],
+["features.messages.impl_MessagesView_Day_3_en","features.messages.impl_MessagesView_Night_3_en",20070,],
+["features.messages.impl_MessagesView_Day_4_en","features.messages.impl_MessagesView_Night_4_en",20070,],
+["features.messages.impl_MessagesView_Day_5_en","features.messages.impl_MessagesView_Night_5_en",20070,],
+["features.messages.impl_MessagesView_Day_6_en","features.messages.impl_MessagesView_Night_6_en",20070,],
+["features.messages.impl_MessagesView_Day_7_en","features.messages.impl_MessagesView_Night_7_en",20070,],
+["features.messages.impl_MessagesView_Day_8_en","features.messages.impl_MessagesView_Night_8_en",20070,],
+["features.messages.impl_MessagesView_Day_9_en","features.messages.impl_MessagesView_Night_9_en",20070,],
["features.migration.impl_MigrationView_Day_0_en","features.migration.impl_MigrationView_Night_0_en",0,],
-["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20056,],
+["features.migration.impl_MigrationView_Day_1_en","features.migration.impl_MigrationView_Night_1_en",20070,],
["libraries.designsystem.theme.components_ModalBottomSheetDark_Bottom_Sheets_en","",0,],
["libraries.designsystem.theme.components_ModalBottomSheetLight_Bottom_Sheets_en","",0,],
["appicon.element_MonochromeIcon_en","",0,],
@@ -555,29 +618,29 @@ export const screenshots = [
["libraries.designsystem.components.list_MutipleSelectionListItemSelectedTrailingContent_Multiple_selection_List_item_-_selection_in_trailing_content_List_items_en","",0,],
["libraries.designsystem.components.list_MutipleSelectionListItemSelected_Multiple_selection_List_item_-_selection_in_supporting_text_List_items_en","",0,],
["libraries.designsystem.components.list_MutipleSelectionListItem_Multiple_selection_List_item_-_no_selection_List_items_en","",0,],
-["features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en","features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20056,],
-["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20056,],
-["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20056,],
+["features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Day_0_en","features.roomlist.impl.components_NativeSlidingSyncMigrationBanner_Night_0_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_0_en","features.preferences.impl.notifications_NotificationSettingsView_Night_0_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_10_en","features.preferences.impl.notifications_NotificationSettingsView_Night_10_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_11_en","features.preferences.impl.notifications_NotificationSettingsView_Night_11_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_12_en","features.preferences.impl.notifications_NotificationSettingsView_Night_12_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_1_en","features.preferences.impl.notifications_NotificationSettingsView_Night_1_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_2_en","features.preferences.impl.notifications_NotificationSettingsView_Night_2_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_3_en","features.preferences.impl.notifications_NotificationSettingsView_Night_3_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_4_en","features.preferences.impl.notifications_NotificationSettingsView_Night_4_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_5_en","features.preferences.impl.notifications_NotificationSettingsView_Night_5_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_6_en","features.preferences.impl.notifications_NotificationSettingsView_Night_6_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_7_en","features.preferences.impl.notifications_NotificationSettingsView_Night_7_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_8_en","features.preferences.impl.notifications_NotificationSettingsView_Night_8_en",20070,],
+["features.preferences.impl.notifications_NotificationSettingsView_Day_9_en","features.preferences.impl.notifications_NotificationSettingsView_Night_9_en",20070,],
+["features.ftue.impl.notifications_NotificationsOptInView_Day_0_en","features.ftue.impl.notifications_NotificationsOptInView_Night_0_en",20070,],
["libraries.oidc.impl.webview_OidcView_Day_0_en","libraries.oidc.impl.webview_OidcView_Night_0_en",0,],
["libraries.oidc.impl.webview_OidcView_Day_1_en","libraries.oidc.impl.webview_OidcView_Night_1_en",0,],
["libraries.designsystem.atomic.pages_OnBoardingPage_Day_0_en","libraries.designsystem.atomic.pages_OnBoardingPage_Night_0_en",0,],
-["features.onboarding.impl_OnBoardingView_Day_0_en","features.onboarding.impl_OnBoardingView_Night_0_en",20056,],
-["features.onboarding.impl_OnBoardingView_Day_1_en","features.onboarding.impl_OnBoardingView_Night_1_en",20056,],
-["features.onboarding.impl_OnBoardingView_Day_2_en","features.onboarding.impl_OnBoardingView_Night_2_en",20056,],
-["features.onboarding.impl_OnBoardingView_Day_3_en","features.onboarding.impl_OnBoardingView_Night_3_en",20056,],
-["features.onboarding.impl_OnBoardingView_Day_4_en","features.onboarding.impl_OnBoardingView_Night_4_en",20056,],
+["features.onboarding.impl_OnBoardingView_Day_0_en","features.onboarding.impl_OnBoardingView_Night_0_en",20070,],
+["features.onboarding.impl_OnBoardingView_Day_1_en","features.onboarding.impl_OnBoardingView_Night_1_en",20070,],
+["features.onboarding.impl_OnBoardingView_Day_2_en","features.onboarding.impl_OnBoardingView_Night_2_en",20070,],
+["features.onboarding.impl_OnBoardingView_Day_3_en","features.onboarding.impl_OnBoardingView_Night_3_en",20070,],
+["features.onboarding.impl_OnBoardingView_Day_4_en","features.onboarding.impl_OnBoardingView_Night_4_en",20070,],
["libraries.designsystem.background_OnboardingBackground_Day_0_en","libraries.designsystem.background_OnboardingBackground_Night_0_en",0,],
["libraries.designsystem.theme.components_OutlinedButtonLargeLowPadding_Buttons_en","",0,],
["libraries.designsystem.theme.components_OutlinedButtonLarge_Buttons_en","",0,],
@@ -589,66 +652,67 @@ export const screenshots = [
["libraries.designsystem.components_PageTitleWithIconFull_Day_2_en","libraries.designsystem.components_PageTitleWithIconFull_Night_2_en",0,],
["libraries.designsystem.components_PageTitleWithIconFull_Day_3_en","libraries.designsystem.components_PageTitleWithIconFull_Night_3_en",0,],
["libraries.designsystem.components_PageTitleWithIconFull_Day_4_en","libraries.designsystem.components_PageTitleWithIconFull_Night_4_en",0,],
+["libraries.designsystem.components_PageTitleWithIconFull_Day_5_en","libraries.designsystem.components_PageTitleWithIconFull_Night_5_en",0,],
["libraries.designsystem.components_PageTitleWithIconMinimal_Day_0_en","libraries.designsystem.components_PageTitleWithIconMinimal_Night_0_en",0,],
-["libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.api.local.pdf_PdfPagesErrorView_Night_0_en",20056,],
-["features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en",20056,],
-["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20056,],
-["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20056,],
-["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20056,],
-["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20056,],
+["libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Day_0_en","libraries.mediaviewer.impl.local.pdf_PdfPagesErrorView_Night_0_en",20070,],
+["features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en","features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en",20070,],
+["libraries.permissions.api_PermissionsView_Day_0_en","libraries.permissions.api_PermissionsView_Night_0_en",20070,],
+["libraries.permissions.api_PermissionsView_Day_1_en","libraries.permissions.api_PermissionsView_Night_1_en",20070,],
+["libraries.permissions.api_PermissionsView_Day_2_en","libraries.permissions.api_PermissionsView_Night_2_en",20070,],
+["libraries.permissions.api_PermissionsView_Day_3_en","libraries.permissions.api_PermissionsView_Night_3_en",20070,],
["features.lockscreen.impl.components_PinEntryTextField_Day_0_en","features.lockscreen.impl.components_PinEntryTextField_Night_0_en",0,],
["libraries.designsystem.components_PinIcon_Day_0_en","libraries.designsystem.components_PinIcon_Night_0_en",0,],
["features.lockscreen.impl.unlock.keypad_PinKeypad_Day_0_en","features.lockscreen.impl.unlock.keypad_PinKeypad_Night_0_en",0,],
-["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20056,],
-["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20056,],
+["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_0_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_0_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_1_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_1_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_2_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_2_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_3_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_3_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_4_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_4_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_5_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_5_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_6_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_6_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockViewInApp_Day_7_en","features.lockscreen.impl.unlock_PinUnlockViewInApp_Night_7_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockView_Day_0_en","features.lockscreen.impl.unlock_PinUnlockView_Night_0_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockView_Day_1_en","features.lockscreen.impl.unlock_PinUnlockView_Night_1_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockView_Day_2_en","features.lockscreen.impl.unlock_PinUnlockView_Night_2_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockView_Day_3_en","features.lockscreen.impl.unlock_PinUnlockView_Night_3_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockView_Day_4_en","features.lockscreen.impl.unlock_PinUnlockView_Night_4_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockView_Day_5_en","features.lockscreen.impl.unlock_PinUnlockView_Night_5_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockView_Day_6_en","features.lockscreen.impl.unlock_PinUnlockView_Night_6_en",20070,],
+["features.lockscreen.impl.unlock_PinUnlockView_Day_7_en","features.lockscreen.impl.unlock_PinUnlockView_Night_7_en",20070,],
["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_0_en",0,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20056,],
-["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20056,],
-["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20056,],
-["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20056,],
-["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20056,],
-["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20056,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_10_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_10_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en",20070,],
+["features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en","features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en",20070,],
+["features.messages.impl.pinned.list_PinnedMessagesListView_Day_0_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_0_en",20070,],
+["features.messages.impl.pinned.list_PinnedMessagesListView_Day_1_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_1_en",20070,],
+["features.messages.impl.pinned.list_PinnedMessagesListView_Day_2_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_2_en",20070,],
+["features.messages.impl.pinned.list_PinnedMessagesListView_Day_3_en","features.messages.impl.pinned.list_PinnedMessagesListView_Night_3_en",20070,],
["libraries.designsystem.atomic.atoms_PlaceholderAtom_Day_0_en","libraries.designsystem.atomic.atoms_PlaceholderAtom_Night_0_en",0,],
-["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20056,],
+["features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedNotSelected_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewDisclosedSelected_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollAnswerViewEndedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedSelected_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerNotSelected_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewEndedWinnerSelected_Night_0_en",20070,],
["features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedNotSelected_Night_0_en",0,],
["features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Day_0_en","features.poll.api.pollcontent_PollAnswerViewUndisclosedSelected_Night_0_en",0,],
-["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20056,],
-["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20056,],
-["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20056,],
-["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20056,],
-["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20056,],
-["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20056,],
-["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20056,],
+["features.poll.api.pollcontent_PollContentViewCreatorEditable_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEditable_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollContentViewCreatorEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewCreatorEnded_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollContentViewCreator_Day_0_en","features.poll.api.pollcontent_PollContentViewCreator_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollContentViewDisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewDisclosed_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollContentViewEnded_Day_0_en","features.poll.api.pollcontent_PollContentViewEnded_Night_0_en",20070,],
+["features.poll.api.pollcontent_PollContentViewUndisclosed_Day_0_en","features.poll.api.pollcontent_PollContentViewUndisclosed_Night_0_en",20070,],
+["features.poll.impl.history_PollHistoryView_Day_0_en","features.poll.impl.history_PollHistoryView_Night_0_en",20070,],
+["features.poll.impl.history_PollHistoryView_Day_1_en","features.poll.impl.history_PollHistoryView_Night_1_en",20070,],
+["features.poll.impl.history_PollHistoryView_Day_2_en","features.poll.impl.history_PollHistoryView_Night_2_en",20070,],
+["features.poll.impl.history_PollHistoryView_Day_3_en","features.poll.impl.history_PollHistoryView_Night_3_en",20070,],
+["features.poll.impl.history_PollHistoryView_Day_4_en","features.poll.impl.history_PollHistoryView_Night_4_en",20070,],
["features.poll.api.pollcontent_PollTitleView_Day_0_en","features.poll.api.pollcontent_PollTitleView_Night_0_en",0,],
["libraries.designsystem.components.preferences_PreferenceCategory_Preferences_en","",0,],
["libraries.designsystem.components.preferences_PreferenceCheckbox_Preferences_en","",0,],
@@ -665,197 +729,197 @@ export const screenshots = [
["libraries.designsystem.components.preferences_PreferenceTextLight_Preferences_en","",0,],
["libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeDark_Preferences_en","",0,],
["libraries.designsystem.components.preferences_PreferenceTextWithEndBadgeLight_Preferences_en","",0,],
-["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20056,],
-["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20056,],
-["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20056,],
-["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20056,],
+["features.preferences.impl.root_PreferencesRootViewDark_0_en","",20070,],
+["features.preferences.impl.root_PreferencesRootViewDark_1_en","",20070,],
+["features.preferences.impl.root_PreferencesRootViewLight_0_en","",20070,],
+["features.preferences.impl.root_PreferencesRootViewLight_1_en","",20070,],
["features.messages.impl.timeline.components.event_ProgressButton_Day_0_en","features.messages.impl.timeline.components.event_ProgressButton_Night_0_en",0,],
-["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20056,],
-["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20056,],
-["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20059,],
-["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20059,],
-["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20059,],
-["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20059,],
-["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20056,],
-["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20056,],
-["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20056,],
-["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20056,],
-["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20056,],
-["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20056,],
-["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20056,],
-["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20056,],
-["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20056,],
-["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20056,],
-["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20056,],
-["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20056,],
-["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20056,],
-["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20056,],
-["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20056,],
-["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20056,],
+["libraries.designsystem.components_ProgressDialogContent_Dialogs_en","",20070,],
+["libraries.designsystem.components_ProgressDialog_Day_0_en","libraries.designsystem.components_ProgressDialog_Night_0_en",20070,],
+["features.messages.impl.timeline.protection_ProtectedView_Day_0_en","features.messages.impl.timeline.protection_ProtectedView_Night_0_en",20070,],
+["features.messages.impl.timeline.protection_ProtectedView_Day_1_en","features.messages.impl.timeline.protection_ProtectedView_Night_1_en",20070,],
+["features.messages.impl.timeline.protection_ProtectedView_Day_2_en","features.messages.impl.timeline.protection_ProtectedView_Night_2_en",20070,],
+["features.messages.impl.timeline.protection_ProtectedView_Day_3_en","features.messages.impl.timeline.protection_ProtectedView_Night_3_en",20070,],
+["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_0_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_0_en",20070,],
+["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_1_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_1_en",20070,],
+["features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Day_2_en","features.login.impl.screens.qrcode.confirmation_QrCodeConfirmationView_Night_2_en",20070,],
+["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_0_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_0_en",20070,],
+["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_1_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_1_en",20070,],
+["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_2_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_2_en",20070,],
+["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_3_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_3_en",20070,],
+["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_4_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_4_en",20070,],
+["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_5_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_5_en",20070,],
+["features.login.impl.screens.qrcode.error_QrCodeErrorView_Day_6_en","features.login.impl.screens.qrcode.error_QrCodeErrorView_Night_6_en",20070,],
+["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_0_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_0_en",20070,],
+["features.login.impl.screens.qrcode.intro_QrCodeIntroView_Day_1_en","features.login.impl.screens.qrcode.intro_QrCodeIntroView_Night_1_en",20070,],
+["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_0_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_0_en",20070,],
+["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_1_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_1_en",20070,],
+["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_2_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_2_en",20070,],
+["features.login.impl.screens.qrcode.scan_QrCodeScanView_Day_3_en","features.login.impl.screens.qrcode.scan_QrCodeScanView_Night_3_en",20070,],
["libraries.designsystem.theme.components_RadioButton_Toggles_en","",0,],
-["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20056,],
-["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20056,],
+["features.rageshake.api.detection_RageshakeDialogContent_Day_0_en","features.rageshake.api.detection_RageshakeDialogContent_Night_0_en",20070,],
+["features.rageshake.api.preferences_RageshakePreferencesView_Day_0_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_0_en",20070,],
["features.rageshake.api.preferences_RageshakePreferencesView_Day_1_en","features.rageshake.api.preferences_RageshakePreferencesView_Night_1_en",0,],
["features.messages.impl.timeline.components.reactionsummary_ReactionSummaryViewContent_Day_0_en","features.messages.impl.timeline.components.reactionsummary_ReactionSummaryViewContent_Night_0_en",0,],
-["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20056,],
-["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20056,],
-["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20056,],
-["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20056,],
-["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20056,],
-["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20056,],
-["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20056,],
+["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_0_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_0_en",20070,],
+["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_1_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_1_en",20070,],
+["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_2_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_2_en",20070,],
+["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_3_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_3_en",20070,],
+["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_4_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_4_en",20070,],
+["features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Day_5_en","features.messages.impl.timeline.components.receipt.bottomsheet_ReadReceiptBottomSheet_Night_5_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_10_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_10_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_11_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_11_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_12_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_12_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_13_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_13_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_8_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_8_en",20070,],
+["features.securebackup.impl.setup.views_RecoveryKeyView_Day_9_en","features.securebackup.impl.setup.views_RecoveryKeyView_Night_9_en",20070,],
["libraries.designsystem.atomic.atoms_RedIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_RedIndicatorAtom_Night_0_en",0,],
["features.messages.impl.timeline.components_ReplySwipeIndicator_Day_0_en","features.messages.impl.timeline.components_ReplySwipeIndicator_Night_0_en",0,],
-["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20056,],
-["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20056,],
-["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20056,],
-["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20056,],
-["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20056,],
-["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20056,],
-["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20056,],
-["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20056,],
-["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20056,],
-["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20056,],
-["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20056,],
-["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20056,],
+["features.messages.impl.report_ReportMessageView_Day_0_en","features.messages.impl.report_ReportMessageView_Night_0_en",20070,],
+["features.messages.impl.report_ReportMessageView_Day_1_en","features.messages.impl.report_ReportMessageView_Night_1_en",20070,],
+["features.messages.impl.report_ReportMessageView_Day_2_en","features.messages.impl.report_ReportMessageView_Night_2_en",20070,],
+["features.messages.impl.report_ReportMessageView_Day_3_en","features.messages.impl.report_ReportMessageView_Night_3_en",20070,],
+["features.messages.impl.report_ReportMessageView_Day_4_en","features.messages.impl.report_ReportMessageView_Night_4_en",20070,],
+["features.messages.impl.report_ReportMessageView_Day_5_en","features.messages.impl.report_ReportMessageView_Night_5_en",20070,],
+["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_0_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_0_en",20070,],
+["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_1_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_1_en",20070,],
+["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_2_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_2_en",20070,],
+["features.securebackup.impl.reset.password_ResetIdentityPasswordView_Day_3_en","features.securebackup.impl.reset.password_ResetIdentityPasswordView_Night_3_en",20070,],
+["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_0_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_0_en",20070,],
+["features.securebackup.impl.reset.root_ResetIdentityRootView_Day_1_en","features.securebackup.impl.reset.root_ResetIdentityRootView_Night_1_en",20070,],
["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_0_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_0_en",0,],
-["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20056,],
-["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20056,],
-["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20056,],
-["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20056,],
-["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en",20056,],
-["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en",20056,],
-["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en",20056,],
-["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en",20056,],
-["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en",20056,],
-["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en",20056,],
-["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en",20056,],
-["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en",20056,],
+["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en",20070,],
+["features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en","features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en",20070,],
+["libraries.designsystem.components.dialogs_RetryDialogContent_Dialogs_en","",20070,],
+["libraries.designsystem.components.dialogs_RetryDialog_Day_0_en","libraries.designsystem.components.dialogs_RetryDialog_Night_0_en",20070,],
+["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_0_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_0_en",20070,],
+["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_1_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_1_en",20070,],
+["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_2_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_2_en",20070,],
+["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_3_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_3_en",20070,],
+["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_4_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_4_en",20070,],
+["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_5_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_5_en",20070,],
+["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_6_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_6_en",20070,],
+["features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Day_7_en","features.roomdetails.impl.rolesandpermissions_RolesAndPermissionsView_Night_7_en",20070,],
["features.roomaliasresolver.impl_RoomAliasResolverView_Day_0_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_0_en",0,],
["features.roomaliasresolver.impl_RoomAliasResolverView_Day_1_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_1_en",0,],
-["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20056,],
-["features.roomdetails.impl_RoomDetailsDark_0_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_10_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_11_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_12_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_13_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_1_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_2_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_3_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_4_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_5_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_6_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_7_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_8_en","",20056,],
-["features.roomdetails.impl_RoomDetailsDark_9_en","",20056,],
-["features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_0_en",20056,],
-["features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_1_en",20056,],
-["features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_2_en",20056,],
-["features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_3_en",20056,],
-["features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_4_en",20056,],
-["features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_5_en",20056,],
-["features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_6_en",20056,],
-["features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_7_en",20056,],
-["features.roomdetails.impl_RoomDetails_0_en","",20056,],
-["features.roomdetails.impl_RoomDetails_10_en","",20056,],
-["features.roomdetails.impl_RoomDetails_11_en","",20056,],
-["features.roomdetails.impl_RoomDetails_12_en","",20056,],
-["features.roomdetails.impl_RoomDetails_13_en","",20056,],
-["features.roomdetails.impl_RoomDetails_1_en","",20056,],
-["features.roomdetails.impl_RoomDetails_2_en","",20056,],
-["features.roomdetails.impl_RoomDetails_3_en","",20056,],
-["features.roomdetails.impl_RoomDetails_4_en","",20056,],
-["features.roomdetails.impl_RoomDetails_5_en","",20056,],
-["features.roomdetails.impl_RoomDetails_6_en","",20056,],
-["features.roomdetails.impl_RoomDetails_7_en","",20056,],
-["features.roomdetails.impl_RoomDetails_8_en","",20056,],
-["features.roomdetails.impl_RoomDetails_9_en","",20056,],
-["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20056,],
-["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20056,],
-["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20056,],
-["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20056,],
-["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20056,],
-["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20056,],
-["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20056,],
-["features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en",20056,],
-["features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en",20056,],
-["features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en",20056,],
-["features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en",20056,],
-["features.roomlist.impl.components_RoomListContentView_Day_0_en","features.roomlist.impl.components_RoomListContentView_Night_0_en",20056,],
-["features.roomlist.impl.components_RoomListContentView_Day_1_en","features.roomlist.impl.components_RoomListContentView_Night_1_en",20056,],
+["features.roomaliasresolver.impl_RoomAliasResolverView_Day_2_en","features.roomaliasresolver.impl_RoomAliasResolverView_Night_2_en",20070,],
+["features.roomdetails.impl_RoomDetailsDark_0_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_10_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_11_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_12_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_13_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_1_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_2_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_3_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_4_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_5_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_6_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_7_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_8_en","",20070,],
+["features.roomdetails.impl_RoomDetailsDark_9_en","",20070,],
+["features.roomdetails.impl.edit_RoomDetailsEditView_Day_0_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_0_en",20070,],
+["features.roomdetails.impl.edit_RoomDetailsEditView_Day_1_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_1_en",20070,],
+["features.roomdetails.impl.edit_RoomDetailsEditView_Day_2_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_2_en",20070,],
+["features.roomdetails.impl.edit_RoomDetailsEditView_Day_3_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_3_en",20070,],
+["features.roomdetails.impl.edit_RoomDetailsEditView_Day_4_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_4_en",20070,],
+["features.roomdetails.impl.edit_RoomDetailsEditView_Day_5_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_5_en",20070,],
+["features.roomdetails.impl.edit_RoomDetailsEditView_Day_6_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_6_en",20070,],
+["features.roomdetails.impl.edit_RoomDetailsEditView_Day_7_en","features.roomdetails.impl.edit_RoomDetailsEditView_Night_7_en",20070,],
+["features.roomdetails.impl_RoomDetails_0_en","",20070,],
+["features.roomdetails.impl_RoomDetails_10_en","",20070,],
+["features.roomdetails.impl_RoomDetails_11_en","",20070,],
+["features.roomdetails.impl_RoomDetails_12_en","",20070,],
+["features.roomdetails.impl_RoomDetails_13_en","",20070,],
+["features.roomdetails.impl_RoomDetails_1_en","",20070,],
+["features.roomdetails.impl_RoomDetails_2_en","",20070,],
+["features.roomdetails.impl_RoomDetails_3_en","",20070,],
+["features.roomdetails.impl_RoomDetails_4_en","",20070,],
+["features.roomdetails.impl_RoomDetails_5_en","",20070,],
+["features.roomdetails.impl_RoomDetails_6_en","",20070,],
+["features.roomdetails.impl_RoomDetails_7_en","",20070,],
+["features.roomdetails.impl_RoomDetails_8_en","",20070,],
+["features.roomdetails.impl_RoomDetails_9_en","",20070,],
+["features.roomdirectory.impl.root_RoomDirectoryView_Day_0_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_0_en",20070,],
+["features.roomdirectory.impl.root_RoomDirectoryView_Day_1_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_1_en",20070,],
+["features.roomdirectory.impl.root_RoomDirectoryView_Day_2_en","features.roomdirectory.impl.root_RoomDirectoryView_Night_2_en",20070,],
+["features.roomdetails.impl.invite_RoomInviteMembersView_Day_0_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_0_en",20070,],
+["features.roomdetails.impl.invite_RoomInviteMembersView_Day_1_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_1_en",20070,],
+["features.roomdetails.impl.invite_RoomInviteMembersView_Day_2_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_2_en",20070,],
+["features.roomdetails.impl.invite_RoomInviteMembersView_Day_3_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_3_en",20070,],
+["features.roomdetails.impl.invite_RoomInviteMembersView_Day_4_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_4_en",20070,],
+["features.roomdetails.impl.invite_RoomInviteMembersView_Day_5_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_5_en",20070,],
+["features.roomdetails.impl.invite_RoomInviteMembersView_Day_6_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_6_en",20070,],
+["features.roomdetails.impl.invite_RoomInviteMembersView_Day_7_en","features.roomdetails.impl.invite_RoomInviteMembersView_Night_7_en",20070,],
+["features.roomlist.impl.components_RoomListContentView_Day_0_en","features.roomlist.impl.components_RoomListContentView_Night_0_en",20070,],
+["features.roomlist.impl.components_RoomListContentView_Day_1_en","features.roomlist.impl.components_RoomListContentView_Night_1_en",20070,],
["features.roomlist.impl.components_RoomListContentView_Day_2_en","features.roomlist.impl.components_RoomListContentView_Night_2_en",0,],
-["features.roomlist.impl.components_RoomListContentView_Day_3_en","features.roomlist.impl.components_RoomListContentView_Night_3_en",20056,],
-["features.roomlist.impl.components_RoomListContentView_Day_4_en","features.roomlist.impl.components_RoomListContentView_Night_4_en",20056,],
-["features.roomlist.impl.components_RoomListContentView_Day_5_en","features.roomlist.impl.components_RoomListContentView_Night_5_en",20056,],
-["features.roomlist.impl.filters_RoomListFiltersView_Day_0_en","features.roomlist.impl.filters_RoomListFiltersView_Night_0_en",20056,],
-["features.roomlist.impl.filters_RoomListFiltersView_Day_1_en","features.roomlist.impl.filters_RoomListFiltersView_Night_1_en",20056,],
-["features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en",20056,],
-["features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en",20056,],
-["features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en",20056,],
+["features.roomlist.impl.components_RoomListContentView_Day_3_en","features.roomlist.impl.components_RoomListContentView_Night_3_en",20070,],
+["features.roomlist.impl.components_RoomListContentView_Day_4_en","features.roomlist.impl.components_RoomListContentView_Night_4_en",20070,],
+["features.roomlist.impl.components_RoomListContentView_Day_5_en","features.roomlist.impl.components_RoomListContentView_Night_5_en",20070,],
+["features.roomlist.impl.filters_RoomListFiltersView_Day_0_en","features.roomlist.impl.filters_RoomListFiltersView_Night_0_en",20070,],
+["features.roomlist.impl.filters_RoomListFiltersView_Day_1_en","features.roomlist.impl.filters_RoomListFiltersView_Night_1_en",20070,],
+["features.roomlist.impl_RoomListModalBottomSheetContent_Day_0_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_0_en",20070,],
+["features.roomlist.impl_RoomListModalBottomSheetContent_Day_1_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_1_en",20070,],
+["features.roomlist.impl_RoomListModalBottomSheetContent_Day_2_en","features.roomlist.impl_RoomListModalBottomSheetContent_Night_2_en",20070,],
["features.roomlist.impl.search_RoomListSearchContent_Day_0_en","features.roomlist.impl.search_RoomListSearchContent_Night_0_en",0,],
-["features.roomlist.impl.search_RoomListSearchContent_Day_1_en","features.roomlist.impl.search_RoomListSearchContent_Night_1_en",20056,],
-["features.roomlist.impl.search_RoomListSearchContent_Day_2_en","features.roomlist.impl.search_RoomListSearchContent_Night_2_en",20056,],
-["features.roomlist.impl_RoomListView_Day_0_en","features.roomlist.impl_RoomListView_Night_0_en",20056,],
-["features.roomlist.impl_RoomListView_Day_10_en","features.roomlist.impl_RoomListView_Night_10_en",20056,],
-["features.roomlist.impl_RoomListView_Day_1_en","features.roomlist.impl_RoomListView_Night_1_en",20056,],
-["features.roomlist.impl_RoomListView_Day_2_en","features.roomlist.impl_RoomListView_Night_2_en",20056,],
-["features.roomlist.impl_RoomListView_Day_3_en","features.roomlist.impl_RoomListView_Night_3_en",20056,],
-["features.roomlist.impl_RoomListView_Day_4_en","features.roomlist.impl_RoomListView_Night_4_en",20056,],
-["features.roomlist.impl_RoomListView_Day_5_en","features.roomlist.impl_RoomListView_Night_5_en",20056,],
-["features.roomlist.impl_RoomListView_Day_6_en","features.roomlist.impl_RoomListView_Night_6_en",20056,],
-["features.roomlist.impl_RoomListView_Day_7_en","features.roomlist.impl_RoomListView_Night_7_en",20056,],
+["features.roomlist.impl.search_RoomListSearchContent_Day_1_en","features.roomlist.impl.search_RoomListSearchContent_Night_1_en",20070,],
+["features.roomlist.impl.search_RoomListSearchContent_Day_2_en","features.roomlist.impl.search_RoomListSearchContent_Night_2_en",20070,],
+["features.roomlist.impl_RoomListView_Day_0_en","features.roomlist.impl_RoomListView_Night_0_en",20070,],
+["features.roomlist.impl_RoomListView_Day_10_en","features.roomlist.impl_RoomListView_Night_10_en",20070,],
+["features.roomlist.impl_RoomListView_Day_1_en","features.roomlist.impl_RoomListView_Night_1_en",20070,],
+["features.roomlist.impl_RoomListView_Day_2_en","features.roomlist.impl_RoomListView_Night_2_en",20070,],
+["features.roomlist.impl_RoomListView_Day_3_en","features.roomlist.impl_RoomListView_Night_3_en",20070,],
+["features.roomlist.impl_RoomListView_Day_4_en","features.roomlist.impl_RoomListView_Night_4_en",20070,],
+["features.roomlist.impl_RoomListView_Day_5_en","features.roomlist.impl_RoomListView_Night_5_en",20070,],
+["features.roomlist.impl_RoomListView_Day_6_en","features.roomlist.impl_RoomListView_Night_6_en",20070,],
+["features.roomlist.impl_RoomListView_Day_7_en","features.roomlist.impl_RoomListView_Night_7_en",20070,],
["features.roomlist.impl_RoomListView_Day_8_en","features.roomlist.impl_RoomListView_Night_8_en",0,],
["features.roomlist.impl_RoomListView_Day_9_en","features.roomlist.impl_RoomListView_Night_9_en",0,],
-["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20056,],
+["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_0_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_0_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_1_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_1_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListViewBanned_Day_2_en","features.roomdetails.impl.members_RoomMemberListViewBanned_Night_2_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListView_Day_0_en","features.roomdetails.impl.members_RoomMemberListView_Night_0_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListView_Day_1_en","features.roomdetails.impl.members_RoomMemberListView_Night_1_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListView_Day_2_en","features.roomdetails.impl.members_RoomMemberListView_Night_2_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListView_Day_3_en","features.roomdetails.impl.members_RoomMemberListView_Night_3_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListView_Day_4_en","features.roomdetails.impl.members_RoomMemberListView_Night_4_en",20070,],
["features.roomdetails.impl.members_RoomMemberListView_Day_5_en","features.roomdetails.impl.members_RoomMemberListView_Night_5_en",0,],
-["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListView_Day_7_en","features.roomdetails.impl.members_RoomMemberListView_Night_7_en",20056,],
-["features.roomdetails.impl.members_RoomMemberListView_Day_8_en","features.roomdetails.impl.members_RoomMemberListView_Night_8_en",20056,],
+["features.roomdetails.impl.members_RoomMemberListView_Day_6_en","features.roomdetails.impl.members_RoomMemberListView_Night_6_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListView_Day_7_en","features.roomdetails.impl.members_RoomMemberListView_Night_7_en",20070,],
+["features.roomdetails.impl.members_RoomMemberListView_Day_8_en","features.roomdetails.impl.members_RoomMemberListView_Night_8_en",20070,],
["libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Day_0_en","libraries.designsystem.atomic.molecules_RoomMembersCountMolecule_Night_0_en",0,],
-["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en",20056,],
-["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en",20056,],
-["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en",20056,],
-["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en",20056,],
-["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en",20056,],
+["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_0_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_0_en",20070,],
+["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_1_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_1_en",20070,],
+["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_2_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_2_en",20070,],
+["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_3_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_3_en",20070,],
+["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_4_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_4_en",20070,],
["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_5_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_5_en",0,],
-["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en",20056,],
-["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en",20056,],
-["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en",20056,],
+["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_6_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_6_en",20070,],
+["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_7_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_7_en",20070,],
+["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_8_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_8_en",20070,],
["features.roomdetails.impl.members.moderation_RoomMembersModerationView_Day_9_en","features.roomdetails.impl.members.moderation_RoomMembersModerationView_Night_9_en",0,],
-["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20056,],
-["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20056,],
-["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20056,],
-["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20056,],
-["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20056,],
-["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20056,],
-["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20056,],
-["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20056,],
-["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20056,],
-["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20056,],
-["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20056,],
-["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20056,],
-["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20056,],
-["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20056,],
+["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsOption_Night_0_en",20070,],
+["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_0_en",20070,],
+["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_1_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_1_en",20070,],
+["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_2_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_2_en",20070,],
+["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_3_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_3_en",20070,],
+["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_4_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_4_en",20070,],
+["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_5_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_5_en",20070,],
+["features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Day_6_en","features.roomdetails.impl.notificationsettings_RoomNotificationSettingsView_Night_6_en",20070,],
+["libraries.roomselect.impl_RoomSelectView_Day_0_en","libraries.roomselect.impl_RoomSelectView_Night_0_en",20070,],
+["libraries.roomselect.impl_RoomSelectView_Day_1_en","libraries.roomselect.impl_RoomSelectView_Night_1_en",20070,],
+["libraries.roomselect.impl_RoomSelectView_Day_2_en","libraries.roomselect.impl_RoomSelectView_Night_2_en",20070,],
+["libraries.roomselect.impl_RoomSelectView_Day_3_en","libraries.roomselect.impl_RoomSelectView_Night_3_en",20070,],
+["libraries.roomselect.impl_RoomSelectView_Day_4_en","libraries.roomselect.impl_RoomSelectView_Night_4_en",20070,],
+["libraries.roomselect.impl_RoomSelectView_Day_5_en","libraries.roomselect.impl_RoomSelectView_Night_5_en",20070,],
["features.roomlist.impl.components_RoomSummaryPlaceholderRow_Day_0_en","features.roomlist.impl.components_RoomSummaryPlaceholderRow_Night_0_en",0,],
["features.roomlist.impl.components_RoomSummaryRow_Day_0_en","features.roomlist.impl.components_RoomSummaryRow_Night_0_en",0,],
["features.roomlist.impl.components_RoomSummaryRow_Day_10_en","features.roomlist.impl.components_RoomSummaryRow_Night_10_en",0,],
@@ -878,12 +942,12 @@ export const screenshots = [
["features.roomlist.impl.components_RoomSummaryRow_Day_26_en","features.roomlist.impl.components_RoomSummaryRow_Night_26_en",0,],
["features.roomlist.impl.components_RoomSummaryRow_Day_27_en","features.roomlist.impl.components_RoomSummaryRow_Night_27_en",0,],
["features.roomlist.impl.components_RoomSummaryRow_Day_28_en","features.roomlist.impl.components_RoomSummaryRow_Night_28_en",0,],
-["features.roomlist.impl.components_RoomSummaryRow_Day_29_en","features.roomlist.impl.components_RoomSummaryRow_Night_29_en",20056,],
-["features.roomlist.impl.components_RoomSummaryRow_Day_2_en","features.roomlist.impl.components_RoomSummaryRow_Night_2_en",20056,],
-["features.roomlist.impl.components_RoomSummaryRow_Day_30_en","features.roomlist.impl.components_RoomSummaryRow_Night_30_en",20056,],
-["features.roomlist.impl.components_RoomSummaryRow_Day_31_en","features.roomlist.impl.components_RoomSummaryRow_Night_31_en",20056,],
-["features.roomlist.impl.components_RoomSummaryRow_Day_32_en","features.roomlist.impl.components_RoomSummaryRow_Night_32_en",20059,],
-["features.roomlist.impl.components_RoomSummaryRow_Day_33_en","features.roomlist.impl.components_RoomSummaryRow_Night_33_en",20059,],
+["features.roomlist.impl.components_RoomSummaryRow_Day_29_en","features.roomlist.impl.components_RoomSummaryRow_Night_29_en",20070,],
+["features.roomlist.impl.components_RoomSummaryRow_Day_2_en","features.roomlist.impl.components_RoomSummaryRow_Night_2_en",20070,],
+["features.roomlist.impl.components_RoomSummaryRow_Day_30_en","features.roomlist.impl.components_RoomSummaryRow_Night_30_en",20070,],
+["features.roomlist.impl.components_RoomSummaryRow_Day_31_en","features.roomlist.impl.components_RoomSummaryRow_Night_31_en",20070,],
+["features.roomlist.impl.components_RoomSummaryRow_Day_32_en","features.roomlist.impl.components_RoomSummaryRow_Night_32_en",20070,],
+["features.roomlist.impl.components_RoomSummaryRow_Day_33_en","features.roomlist.impl.components_RoomSummaryRow_Night_33_en",20070,],
["features.roomlist.impl.components_RoomSummaryRow_Day_3_en","features.roomlist.impl.components_RoomSummaryRow_Night_3_en",0,],
["features.roomlist.impl.components_RoomSummaryRow_Day_4_en","features.roomlist.impl.components_RoomSummaryRow_Night_4_en",0,],
["features.roomlist.impl.components_RoomSummaryRow_Day_5_en","features.roomlist.impl.components_RoomSummaryRow_Night_5_en",0,],
@@ -891,59 +955,59 @@ export const screenshots = [
["features.roomlist.impl.components_RoomSummaryRow_Day_7_en","features.roomlist.impl.components_RoomSummaryRow_Night_7_en",0,],
["features.roomlist.impl.components_RoomSummaryRow_Day_8_en","features.roomlist.impl.components_RoomSummaryRow_Night_8_en",0,],
["features.roomlist.impl.components_RoomSummaryRow_Day_9_en","features.roomlist.impl.components_RoomSummaryRow_Night_9_en",0,],
-["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20056,],
-["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20056,],
-["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20056,],
-["appicon.enterprise_RoundIcon_en","",0,],
+["appnav.root_RootView_Day_0_en","appnav.root_RootView_Night_0_en",20070,],
+["appnav.root_RootView_Day_1_en","appnav.root_RootView_Night_1_en",20070,],
+["appnav.root_RootView_Day_2_en","appnav.root_RootView_Night_2_en",20070,],
["appicon.element_RoundIcon_en","",0,],
+["appicon.enterprise_RoundIcon_en","",0,],
["libraries.designsystem.atomic.atoms_RoundedIconAtom_Day_0_en","libraries.designsystem.atomic.atoms_RoundedIconAtom_Night_0_en",0,],
-["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20056,],
-["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20056,],
-["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20056,],
+["features.verifysession.impl.emoji_SasEmojis_Day_0_en","features.verifysession.impl.emoji_SasEmojis_Night_0_en",20070,],
+["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en",20070,],
+["features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en","features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en",20070,],
["libraries.designsystem.theme.components_SearchBarActiveNoneQuery_Search_views_en","",0,],
["libraries.designsystem.theme.components_SearchBarActiveWithContent_Search_views_en","",0,],
-["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20056,],
+["libraries.designsystem.theme.components_SearchBarActiveWithNoResults_Search_views_en","",20070,],
["libraries.designsystem.theme.components_SearchBarActiveWithQueryNoBackButton_Search_views_en","",0,],
["libraries.designsystem.theme.components_SearchBarActiveWithQuery_Search_views_en","",0,],
["libraries.designsystem.theme.components_SearchBarInactive_Search_views_en","",0,],
-["features.createroom.impl.components_SearchMultipleUsersResultItem_en","",20056,],
-["features.createroom.impl.components_SearchSingleUserResultItem_en","",20056,],
-["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20056,],
-["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20056,],
-["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20056,],
-["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20056,],
-["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20056,],
-["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20056,],
-["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20056,],
-["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20056,],
-["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20056,],
-["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20056,],
+["features.createroom.impl.components_SearchMultipleUsersResultItem_en","",20070,],
+["features.createroom.impl.components_SearchSingleUserResultItem_en","",20070,],
+["features.securebackup.impl.disable_SecureBackupDisableView_Day_0_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_0_en",20070,],
+["features.securebackup.impl.disable_SecureBackupDisableView_Day_1_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_1_en",20070,],
+["features.securebackup.impl.disable_SecureBackupDisableView_Day_2_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_2_en",20070,],
+["features.securebackup.impl.disable_SecureBackupDisableView_Day_3_en","features.securebackup.impl.disable_SecureBackupDisableView_Night_3_en",20070,],
+["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_0_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_0_en",20070,],
+["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_1_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_1_en",20070,],
+["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_2_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_2_en",20070,],
+["features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Day_3_en","features.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_Night_3_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_0_en","features.securebackup.impl.root_SecureBackupRootView_Night_0_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_10_en","features.securebackup.impl.root_SecureBackupRootView_Night_10_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_11_en","features.securebackup.impl.root_SecureBackupRootView_Night_11_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_12_en","features.securebackup.impl.root_SecureBackupRootView_Night_12_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_13_en","features.securebackup.impl.root_SecureBackupRootView_Night_13_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_14_en","features.securebackup.impl.root_SecureBackupRootView_Night_14_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_15_en","features.securebackup.impl.root_SecureBackupRootView_Night_15_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_16_en","features.securebackup.impl.root_SecureBackupRootView_Night_16_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_17_en","features.securebackup.impl.root_SecureBackupRootView_Night_17_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_1_en","features.securebackup.impl.root_SecureBackupRootView_Night_1_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_2_en","features.securebackup.impl.root_SecureBackupRootView_Night_2_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_3_en","features.securebackup.impl.root_SecureBackupRootView_Night_3_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_4_en","features.securebackup.impl.root_SecureBackupRootView_Night_4_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_5_en","features.securebackup.impl.root_SecureBackupRootView_Night_5_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_6_en","features.securebackup.impl.root_SecureBackupRootView_Night_6_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_7_en","features.securebackup.impl.root_SecureBackupRootView_Night_7_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_8_en","features.securebackup.impl.root_SecureBackupRootView_Night_8_en",20070,],
+["features.securebackup.impl.root_SecureBackupRootView_Day_9_en","features.securebackup.impl.root_SecureBackupRootView_Night_9_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en",20070,],
+["features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en","features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en",20070,],
["libraries.matrix.ui.components_SelectedRoom_Day_0_en","libraries.matrix.ui.components_SelectedRoom_Night_0_en",0,],
["libraries.matrix.ui.components_SelectedRoom_Day_1_en","libraries.matrix.ui.components_SelectedRoom_Night_1_en",0,],
["libraries.matrix.ui.components_SelectedRoom_Day_2_en","libraries.matrix.ui.components_SelectedRoom_Night_2_en",0,],
@@ -951,11 +1015,11 @@ export const screenshots = [
["libraries.matrix.ui.components_SelectedUser_Day_0_en","libraries.matrix.ui.components_SelectedUser_Night_0_en",0,],
["libraries.matrix.ui.components_SelectedUsersRowList_Day_0_en","libraries.matrix.ui.components_SelectedUsersRowList_Night_0_en",0,],
["libraries.textcomposer.components_SendButton_Day_0_en","libraries.textcomposer.components_SendButton_Night_0_en",0,],
-["features.location.impl.send_SendLocationView_Day_0_en","features.location.impl.send_SendLocationView_Night_0_en",20056,],
-["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",20056,],
-["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",20056,],
-["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",20056,],
-["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",20056,],
+["features.location.impl.send_SendLocationView_Day_0_en","features.location.impl.send_SendLocationView_Night_0_en",20070,],
+["features.location.impl.send_SendLocationView_Day_1_en","features.location.impl.send_SendLocationView_Night_1_en",20070,],
+["features.location.impl.send_SendLocationView_Day_2_en","features.location.impl.send_SendLocationView_Night_2_en",20070,],
+["features.location.impl.send_SendLocationView_Day_3_en","features.location.impl.send_SendLocationView_Night_3_en",20070,],
+["features.location.impl.send_SendLocationView_Day_4_en","features.location.impl.send_SendLocationView_Night_4_en",20070,],
["libraries.matrix.ui.messages.sender_SenderName_Day_0_en","libraries.matrix.ui.messages.sender_SenderName_Night_0_en",0,],
["libraries.matrix.ui.messages.sender_SenderName_Day_1_en","libraries.matrix.ui.messages.sender_SenderName_Night_1_en",0,],
["libraries.matrix.ui.messages.sender_SenderName_Day_2_en","libraries.matrix.ui.messages.sender_SenderName_Night_2_en",0,],
@@ -965,27 +1029,27 @@ export const screenshots = [
["libraries.matrix.ui.messages.sender_SenderName_Day_6_en","libraries.matrix.ui.messages.sender_SenderName_Night_6_en",0,],
["libraries.matrix.ui.messages.sender_SenderName_Day_7_en","libraries.matrix.ui.messages.sender_SenderName_Night_7_en",0,],
["libraries.matrix.ui.messages.sender_SenderName_Day_8_en","libraries.matrix.ui.messages.sender_SenderName_Night_8_en",0,],
-["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20059,],
-["features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20056,],
-["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20056,],
-["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20056,],
-["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20056,],
-["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20056,],
-["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20056,],
-["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20056,],
+["features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en","features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en",20070,],
+["features.roomlist.impl.components_SetUpRecoveryKeyBanner_Day_0_en","features.roomlist.impl.components_SetUpRecoveryKeyBanner_Night_0_en",20070,],
+["features.lockscreen.impl.setup.biometric_SetupBiometricView_Day_0_en","features.lockscreen.impl.setup.biometric_SetupBiometricView_Night_0_en",20070,],
+["features.lockscreen.impl.setup.pin_SetupPinView_Day_0_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_0_en",20070,],
+["features.lockscreen.impl.setup.pin_SetupPinView_Day_1_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_1_en",20070,],
+["features.lockscreen.impl.setup.pin_SetupPinView_Day_2_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_2_en",20070,],
+["features.lockscreen.impl.setup.pin_SetupPinView_Day_3_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_3_en",20070,],
+["features.lockscreen.impl.setup.pin_SetupPinView_Day_4_en","features.lockscreen.impl.setup.pin_SetupPinView_Night_4_en",20070,],
["features.share.impl_ShareView_Day_0_en","features.share.impl_ShareView_Night_0_en",0,],
["features.share.impl_ShareView_Day_1_en","features.share.impl_ShareView_Night_1_en",0,],
["features.share.impl_ShareView_Day_2_en","features.share.impl_ShareView_Night_2_en",0,],
-["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20056,],
-["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20056,],
-["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20056,],
-["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20056,],
-["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20056,],
-["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20056,],
-["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20056,],
-["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20056,],
-["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20056,],
-["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20056,],
+["features.share.impl_ShareView_Day_3_en","features.share.impl_ShareView_Night_3_en",20070,],
+["features.location.impl.show_ShowLocationView_Day_0_en","features.location.impl.show_ShowLocationView_Night_0_en",20070,],
+["features.location.impl.show_ShowLocationView_Day_1_en","features.location.impl.show_ShowLocationView_Night_1_en",20070,],
+["features.location.impl.show_ShowLocationView_Day_2_en","features.location.impl.show_ShowLocationView_Night_2_en",20070,],
+["features.location.impl.show_ShowLocationView_Day_3_en","features.location.impl.show_ShowLocationView_Night_3_en",20070,],
+["features.location.impl.show_ShowLocationView_Day_4_en","features.location.impl.show_ShowLocationView_Night_4_en",20070,],
+["features.location.impl.show_ShowLocationView_Day_5_en","features.location.impl.show_ShowLocationView_Night_5_en",20070,],
+["features.location.impl.show_ShowLocationView_Day_6_en","features.location.impl.show_ShowLocationView_Night_6_en",20070,],
+["features.location.impl.show_ShowLocationView_Day_7_en","features.location.impl.show_ShowLocationView_Night_7_en",20070,],
+["features.signedout.impl_SignedOutView_Day_0_en","features.signedout.impl_SignedOutView_Night_0_en",20070,],
["libraries.designsystem.components.dialogs_SingleSelectionDialogContent_Dialogs_en","",0,],
["libraries.designsystem.components.dialogs_SingleSelectionDialog_Day_0_en","libraries.designsystem.components.dialogs_SingleSelectionDialog_Night_0_en",0,],
["libraries.designsystem.components.list_SingleSelectionListItemCustomFormattert_Single_selection_List_item_-_custom_formatter_List_items_en","",0,],
@@ -994,7 +1058,7 @@ export const screenshots = [
["libraries.designsystem.components.list_SingleSelectionListItemUnselectedWithSupportingText_Single_selection_List_item_-_no_selection,_supporting_text_List_items_en","",0,],
["libraries.designsystem.components.list_SingleSelectionListItem_Single_selection_List_item_-_no_selection_List_items_en","",0,],
["libraries.designsystem.theme.components_Sliders_Sliders_en","",0,],
-["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20056,],
+["features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Day_0_en","features.login.impl.dialogs_SlidingSyncNotSupportedDialog_Night_0_en",20070,],
["libraries.designsystem.theme.components_SnackbarWithActionAndCloseButton_Snackbar_with_action_and_close_button_Snackbars_en","",0,],
["libraries.designsystem.theme.components_SnackbarWithActionOnNewLineAndCloseButton_Snackbar_with_action_and_close_button_on_new_line_Snackbars_en","",0,],
["libraries.designsystem.theme.components_SnackbarWithActionOnNewLine_Snackbar_with_action_on_new_line_Snackbars_en","",0,],
@@ -1004,40 +1068,40 @@ export const screenshots = [
["libraries.designsystem.modifiers_SquareSizeModifierLargeHeight_en","",0,],
["libraries.designsystem.modifiers_SquareSizeModifierLargeWidth_en","",0,],
["features.location.api.internal_StaticMapPlaceholder_Day_0_en","features.location.api.internal_StaticMapPlaceholder_Night_0_en",0,],
-["features.location.api.internal_StaticMapPlaceholder_Day_1_en","features.location.api.internal_StaticMapPlaceholder_Night_1_en",20056,],
+["features.location.api.internal_StaticMapPlaceholder_Day_1_en","features.location.api.internal_StaticMapPlaceholder_Night_1_en",20070,],
["features.location.api_StaticMapView_Day_0_en","features.location.api_StaticMapView_Night_0_en",0,],
-["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20056,],
+["features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Day_0_en","features.messages.impl.messagecomposer.suggestions_SuggestionsPickerView_Night_0_en",20070,],
["libraries.designsystem.atomic.pages_SunsetPage_Day_0_en","libraries.designsystem.atomic.pages_SunsetPage_Night_0_en",0,],
["libraries.designsystem.components.button_SuperButton_Day_0_en","libraries.designsystem.components.button_SuperButton_Night_0_en",0,],
["libraries.designsystem.theme.components_Surface_en","",0,],
["libraries.designsystem.theme.components_Switch_Toggles_en","",0,],
-["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20056,],
+["appnav.loggedin_SyncStateView_Day_0_en","appnav.loggedin_SyncStateView_Night_0_en",20070,],
["libraries.designsystem.theme.components_TextButtonLargeLowPadding_Buttons_en","",0,],
["libraries.designsystem.theme.components_TextButtonLarge_Buttons_en","",0,],
["libraries.designsystem.theme.components_TextButtonMediumLowPadding_Buttons_en","",0,],
["libraries.designsystem.theme.components_TextButtonMedium_Buttons_en","",0,],
["libraries.designsystem.theme.components_TextButtonSmall_Buttons_en","",0,],
-["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20056,],
-["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20059,],
-["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20056,],
-["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20056,],
-["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20056,],
-["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20056,],
-["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20056,],
-["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20056,],
-["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20056,],
-["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20056,],
+["libraries.textcomposer_TextComposerAddCaption_Day_0_en","libraries.textcomposer_TextComposerAddCaption_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerCaption_Day_0_en","libraries.textcomposer_TextComposerCaption_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerEditCaption_Day_0_en","libraries.textcomposer_TextComposerEditCaption_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerEdit_Day_0_en","libraries.textcomposer_TextComposerEdit_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerFormatting_Day_0_en","libraries.textcomposer_TextComposerFormatting_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLinkWithoutText_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerLinkDialogCreateLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogCreateLink_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerLinkDialogEditLink_Day_0_en","libraries.textcomposer_TextComposerLinkDialogEditLink_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_0_en","libraries.textcomposer_TextComposerReply_Night_0_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_10_en","libraries.textcomposer_TextComposerReply_Night_10_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_11_en","libraries.textcomposer_TextComposerReply_Night_11_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_1_en","libraries.textcomposer_TextComposerReply_Night_1_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_2_en","libraries.textcomposer_TextComposerReply_Night_2_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_3_en","libraries.textcomposer_TextComposerReply_Night_3_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_4_en","libraries.textcomposer_TextComposerReply_Night_4_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_5_en","libraries.textcomposer_TextComposerReply_Night_5_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_6_en","libraries.textcomposer_TextComposerReply_Night_6_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_7_en","libraries.textcomposer_TextComposerReply_Night_7_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_8_en","libraries.textcomposer_TextComposerReply_Night_8_en",20070,],
+["libraries.textcomposer_TextComposerReply_Day_9_en","libraries.textcomposer_TextComposerReply_Night_9_en",20070,],
+["libraries.textcomposer_TextComposerSimple_Day_0_en","libraries.textcomposer_TextComposerSimple_Night_0_en",20070,],
["libraries.textcomposer_TextComposerVoice_Day_0_en","libraries.textcomposer_TextComposerVoice_Night_0_en",0,],
["libraries.designsystem.theme.components_TextDark_Text_en","",0,],
["libraries.designsystem.components.list_TextFieldListItemEmpty_Text_field_List_item_-_empty_List_items_en","",0,],
@@ -1047,14 +1111,14 @@ export const screenshots = [
["libraries.designsystem.theme.components_TextFieldsLight_TextFields_en","",0,],
["libraries.textcomposer.components_TextFormatting_Day_0_en","libraries.textcomposer.components_TextFormatting_Night_0_en",0,],
["libraries.designsystem.theme.components_TextLight_Text_en","",0,],
-["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20056,],
-["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20056,],
-["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20056,],
+["libraries.designsystem.theme.components.previews_TimePickerHorizontal_DateTime_pickers_en","",20070,],
+["libraries.designsystem.theme.components.previews_TimePickerVerticalDark_DateTime_pickers_en","",20070,],
+["libraries.designsystem.theme.components.previews_TimePickerVerticalLight_DateTime_pickers_en","",20070,],
["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_0_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_0_en",0,],
["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_1_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_1_en",0,],
["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_2_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_2_en",0,],
-["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20056,],
-["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20056,],
+["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_3_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_3_en",20070,],
+["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_4_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_4_en",20070,],
["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en",0,],
["features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en","features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en",0,],
["features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en",0,],
@@ -1063,14 +1127,17 @@ export const screenshots = [
["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_2_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_3_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemAudioView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemAudioView_Night_4_en",0,],
-["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20056,],
+["features.messages.impl.timeline.components_TimelineItemCallNotifyView_Day_0_en","features.messages.impl.timeline.components_TimelineItemCallNotifyView_Night_0_en",20070,],
["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_0_en",0,],
["features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Day_1_en","features.messages.impl.timeline.components.virtual_TimelineItemDaySeparatorView_Night_1_en",0,],
-["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20056,],
-["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20056,],
-["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20059,],
-["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20059,],
-["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20056,],
+["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_0_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_1_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_2_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_3_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_4_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en","features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en",20070,],
["features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowLongSenderName_en","",0,],
@@ -1078,17 +1145,17 @@ export const screenshots = [
["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_0_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_1_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_2_en",0,],
-["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20056,],
-["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20056,],
+["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_3_en",20070,],
+["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_4_en",20070,],
["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en",0,],
-["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20056,],
-["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20056,],
+["features.messages.impl.timeline.components_TimelineItemEventRowUtd_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowUtd_Night_0_en",20070,],
+["features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithManyReactions_Night_0_en",20070,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_0_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_1_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithRR_Night_2_en",0,],
-["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20056,],
-["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20056,],
+["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en",20070,],
+["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en",20070,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en",0,],
@@ -1097,40 +1164,40 @@ export const screenshots = [
["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en",0,],
-["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20056,],
+["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en",20070,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en",0,],
-["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20056,],
+["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en",20070,],
["features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en","features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en",0,],
["features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en",0,],
-["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20056,],
+["features.messages.impl.timeline.components_TimelineItemEventTimestampBelow_en","",20070,],
["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_0_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_1_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_2_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_3_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemFileView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemFileView_Night_4_en",0,],
-["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20056,],
-["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20056,],
-["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20059,],
+["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentCollapse_Night_0_en",20070,],
+["features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Day_0_en","features.messages.impl.timeline.components_TimelineItemGroupedEventsRowContentExpanded_Night_0_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageViewHideMediaContent_Night_0_en",20070,],
["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemInformativeView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemInformativeView_Night_0_en",0,],
-["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20059,],
+["features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLegacyCallInviteView_Night_0_en",20070,],
["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_0_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemLocationView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemLocationView_Night_1_en",0,],
-["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20056,],
-["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20056,],
-["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20056,],
-["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20056,],
-["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20056,],
+["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_0_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_1_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_2_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemPollView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemPollView_Night_3_en",20070,],
+["features.messages.impl.timeline.components_TimelineItemReactionsLayout_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsLayout_Night_0_en",20070,],
["features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewFew_Night_0_en",0,],
-["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20056,],
-["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20056,],
+["features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewIncoming_Night_0_en",20070,],
+["features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsViewOutgoing_Night_0_en",20070,],
["features.messages.impl.timeline.components_TimelineItemReactionsView_Day_0_en","features.messages.impl.timeline.components_TimelineItemReactionsView_Night_0_en",0,],
-["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20056,],
+["features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemReadMarkerView_Night_0_en",20070,],
["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_0_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_0_en",0,],
["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_1_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_1_en",0,],
["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_2_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_2_en",0,],
@@ -1139,8 +1206,8 @@ export const screenshots = [
["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_5_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_5_en",0,],
["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_6_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_6_en",0,],
["features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Day_7_en","features.messages.impl.timeline.components.receipt_TimelineItemReadReceiptView_Night_7_en",0,],
-["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20056,],
-["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20056,],
+["features.messages.impl.timeline.components.event_TimelineItemRedactedView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemRedactedView_Night_0_en",20070,],
+["features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineItemRoomBeginningView_Night_0_en",20070,],
["features.messages.impl.timeline.components_TimelineItemStateEventRow_Day_0_en","features.messages.impl.timeline.components_TimelineItemStateEventRow_Night_0_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemStateView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStateView_Night_0_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en",0,],
@@ -1153,8 +1220,8 @@ export const screenshots = [
["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_3_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_3_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_4_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_4_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemTextView_Day_5_en","features.messages.impl.timeline.components.event_TimelineItemTextView_Night_5_en",0,],
-["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20056,],
-["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20059,],
+["features.messages.impl.timeline.components.event_TimelineItemUnknownView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemUnknownView_Night_0_en",20070,],
+["features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoViewHideMediaContent_Night_0_en",20070,],
["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en",0,],
["features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en","features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en",0,],
@@ -1177,85 +1244,87 @@ export const screenshots = [
["features.messages.impl.timeline.components.event_TimelineItemVoiceView_Day_9_en","features.messages.impl.timeline.components.event_TimelineItemVoiceView_Night_9_en",0,],
["features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Day_0_en","features.messages.impl.timeline.components.virtual_TimelineLoadingMoreIndicator_Night_0_en",0,],
["features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en","features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en",0,],
-["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20056,],
-["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20056,],
+["features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en","features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en",20070,],
+["features.messages.impl.timeline_TimelineView_Day_0_en","features.messages.impl.timeline_TimelineView_Night_0_en",20070,],
["features.messages.impl.timeline_TimelineView_Day_10_en","features.messages.impl.timeline_TimelineView_Night_10_en",0,],
-["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20056,],
-["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20056,],
-["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20056,],
-["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20056,],
-["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20056,],
-["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20056,],
-["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20056,],
-["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20056,],
+["features.messages.impl.timeline_TimelineView_Day_11_en","features.messages.impl.timeline_TimelineView_Night_11_en",20070,],
+["features.messages.impl.timeline_TimelineView_Day_12_en","features.messages.impl.timeline_TimelineView_Night_12_en",20070,],
+["features.messages.impl.timeline_TimelineView_Day_13_en","features.messages.impl.timeline_TimelineView_Night_13_en",20070,],
+["features.messages.impl.timeline_TimelineView_Day_14_en","features.messages.impl.timeline_TimelineView_Night_14_en",20070,],
+["features.messages.impl.timeline_TimelineView_Day_15_en","features.messages.impl.timeline_TimelineView_Night_15_en",20070,],
+["features.messages.impl.timeline_TimelineView_Day_16_en","features.messages.impl.timeline_TimelineView_Night_16_en",20070,],
+["features.messages.impl.timeline_TimelineView_Day_17_en","features.messages.impl.timeline_TimelineView_Night_17_en",20070,],
+["features.messages.impl.timeline_TimelineView_Day_1_en","features.messages.impl.timeline_TimelineView_Night_1_en",20070,],
["features.messages.impl.timeline_TimelineView_Day_2_en","features.messages.impl.timeline_TimelineView_Night_2_en",0,],
["features.messages.impl.timeline_TimelineView_Day_3_en","features.messages.impl.timeline_TimelineView_Night_3_en",0,],
-["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20056,],
+["features.messages.impl.timeline_TimelineView_Day_4_en","features.messages.impl.timeline_TimelineView_Night_4_en",20070,],
["features.messages.impl.timeline_TimelineView_Day_5_en","features.messages.impl.timeline_TimelineView_Night_5_en",0,],
-["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20056,],
+["features.messages.impl.timeline_TimelineView_Day_6_en","features.messages.impl.timeline_TimelineView_Night_6_en",20070,],
["features.messages.impl.timeline_TimelineView_Day_7_en","features.messages.impl.timeline_TimelineView_Night_7_en",0,],
-["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",20056,],
+["features.messages.impl.timeline_TimelineView_Day_8_en","features.messages.impl.timeline_TimelineView_Night_8_en",20070,],
["features.messages.impl.timeline_TimelineView_Day_9_en","features.messages.impl.timeline_TimelineView_Night_9_en",0,],
["libraries.designsystem.theme.components_TopAppBar_App_Bars_en","",0,],
-["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20056,],
-["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20056,],
-["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20056,],
-["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20056,],
-["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20056,],
-["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20056,],
-["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20056,],
-["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20056,],
+["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_0_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_0_en",20070,],
+["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_1_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_1_en",20070,],
+["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_2_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_2_en",20070,],
+["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_3_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_3_en",20070,],
+["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_4_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_4_en",20070,],
+["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_5_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_5_en",20070,],
+["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_6_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_6_en",20070,],
+["libraries.troubleshoot.impl_TroubleshootNotificationsView_Day_7_en","libraries.troubleshoot.impl_TroubleshootNotificationsView_Night_7_en",20070,],
["features.messages.impl.typing_TypingNotificationView_Day_0_en","features.messages.impl.typing_TypingNotificationView_Night_0_en",0,],
-["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20056,],
-["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20056,],
-["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20056,],
-["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20056,],
-["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20056,],
-["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20056,],
+["features.messages.impl.typing_TypingNotificationView_Day_1_en","features.messages.impl.typing_TypingNotificationView_Night_1_en",20070,],
+["features.messages.impl.typing_TypingNotificationView_Day_2_en","features.messages.impl.typing_TypingNotificationView_Night_2_en",20070,],
+["features.messages.impl.typing_TypingNotificationView_Day_3_en","features.messages.impl.typing_TypingNotificationView_Night_3_en",20070,],
+["features.messages.impl.typing_TypingNotificationView_Day_4_en","features.messages.impl.typing_TypingNotificationView_Night_4_en",20070,],
+["features.messages.impl.typing_TypingNotificationView_Day_5_en","features.messages.impl.typing_TypingNotificationView_Night_5_en",20070,],
+["features.messages.impl.typing_TypingNotificationView_Day_6_en","features.messages.impl.typing_TypingNotificationView_Night_6_en",20070,],
["features.messages.impl.typing_TypingNotificationView_Day_7_en","features.messages.impl.typing_TypingNotificationView_Night_7_en",0,],
["features.messages.impl.typing_TypingNotificationView_Day_8_en","features.messages.impl.typing_TypingNotificationView_Night_8_en",0,],
["libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Day_0_en","libraries.designsystem.atomic.atoms_UnreadIndicatorAtom_Night_0_en",0,],
-["libraries.matrix.ui.components_UnresolvedUserRow_en","",20056,],
+["libraries.matrix.ui.components_UnresolvedUserRow_en","",20070,],
["libraries.matrix.ui.components_UnsavedAvatar_Day_0_en","libraries.matrix.ui.components_UnsavedAvatar_Night_0_en",0,],
["libraries.designsystem.components.avatar_UserAvatarColors_Day_0_en","libraries.designsystem.components.avatar_UserAvatarColors_Night_0_en",0,],
-["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20056,],
-["features.createroom.impl.components_UserListView_Day_0_en","features.createroom.impl.components_UserListView_Night_0_en",20056,],
-["features.createroom.impl.components_UserListView_Day_1_en","features.createroom.impl.components_UserListView_Night_1_en",20056,],
-["features.createroom.impl.components_UserListView_Day_2_en","features.createroom.impl.components_UserListView_Night_2_en",20056,],
+["features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Day_0_en","features.roomdetails.impl.notificationsettings_UserDefinedRoomNotificationSettingsView_Night_0_en",20070,],
+["features.createroom.impl.components_UserListView_Day_0_en","features.createroom.impl.components_UserListView_Night_0_en",20070,],
+["features.createroom.impl.components_UserListView_Day_1_en","features.createroom.impl.components_UserListView_Night_1_en",20070,],
+["features.createroom.impl.components_UserListView_Day_2_en","features.createroom.impl.components_UserListView_Night_2_en",20070,],
["features.createroom.impl.components_UserListView_Day_3_en","features.createroom.impl.components_UserListView_Night_3_en",0,],
["features.createroom.impl.components_UserListView_Day_4_en","features.createroom.impl.components_UserListView_Night_4_en",0,],
["features.createroom.impl.components_UserListView_Day_5_en","features.createroom.impl.components_UserListView_Night_5_en",0,],
["features.createroom.impl.components_UserListView_Day_6_en","features.createroom.impl.components_UserListView_Night_6_en",0,],
-["features.createroom.impl.components_UserListView_Day_7_en","features.createroom.impl.components_UserListView_Night_7_en",20056,],
+["features.createroom.impl.components_UserListView_Day_7_en","features.createroom.impl.components_UserListView_Night_7_en",20070,],
["features.createroom.impl.components_UserListView_Day_8_en","features.createroom.impl.components_UserListView_Night_8_en",0,],
-["features.createroom.impl.components_UserListView_Day_9_en","features.createroom.impl.components_UserListView_Night_9_en",20056,],
+["features.createroom.impl.components_UserListView_Day_9_en","features.createroom.impl.components_UserListView_Night_9_en",20070,],
["features.preferences.impl.user_UserPreferences_Day_0_en","features.preferences.impl.user_UserPreferences_Night_0_en",0,],
["features.preferences.impl.user_UserPreferences_Day_1_en","features.preferences.impl.user_UserPreferences_Night_1_en",0,],
["features.preferences.impl.user_UserPreferences_Day_2_en","features.preferences.impl.user_UserPreferences_Night_2_en",0,],
-["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20059,],
-["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20056,],
-["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20056,],
-["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20056,],
-["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20056,],
-["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20056,],
-["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20056,],
-["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20056,],
-["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en",20056,],
+["features.userprofile.shared_UserProfileHeaderSection_Day_0_en","features.userprofile.shared_UserProfileHeaderSection_Night_0_en",20070,],
+["features.userprofile.shared_UserProfileView_Day_0_en","features.userprofile.shared_UserProfileView_Night_0_en",20070,],
+["features.userprofile.shared_UserProfileView_Day_1_en","features.userprofile.shared_UserProfileView_Night_1_en",20070,],
+["features.userprofile.shared_UserProfileView_Day_2_en","features.userprofile.shared_UserProfileView_Night_2_en",20070,],
+["features.userprofile.shared_UserProfileView_Day_3_en","features.userprofile.shared_UserProfileView_Night_3_en",20070,],
+["features.userprofile.shared_UserProfileView_Day_4_en","features.userprofile.shared_UserProfileView_Night_4_en",20070,],
+["features.userprofile.shared_UserProfileView_Day_5_en","features.userprofile.shared_UserProfileView_Night_5_en",20070,],
+["features.userprofile.shared_UserProfileView_Day_6_en","features.userprofile.shared_UserProfileView_Night_6_en",20070,],
+["features.userprofile.shared_UserProfileView_Day_7_en","features.userprofile.shared_UserProfileView_Night_7_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en",20070,],
["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en",0,],
["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_12_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_12_en",0,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en",20056,],
-["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en",20056,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_13_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_13_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en",20070,],
+["features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en","features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en",20070,],
["libraries.designsystem.ruler_VerticalRuler_Day_0_en","libraries.designsystem.ruler_VerticalRuler_Night_0_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en",0,],
+["libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en","libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en",0,],
["features.viewfolder.impl.file_ViewFileView_Day_0_en","features.viewfolder.impl.file_ViewFileView_Night_0_en",0,],
["features.viewfolder.impl.file_ViewFileView_Day_1_en","features.viewfolder.impl.file_ViewFileView_Night_1_en",0,],
["features.viewfolder.impl.file_ViewFileView_Day_2_en","features.viewfolder.impl.file_ViewFileView_Night_2_en",0,],
@@ -1269,6 +1338,6 @@ export const screenshots = [
["libraries.textcomposer.components_VoiceMessageRecording_Day_0_en","libraries.textcomposer.components_VoiceMessageRecording_Night_0_en",0,],
["libraries.textcomposer.components_VoiceMessage_Day_0_en","libraries.textcomposer.components_VoiceMessage_Night_0_en",0,],
["libraries.designsystem.components.media_WaveformPlaybackView_Day_0_en","libraries.designsystem.components.media_WaveformPlaybackView_Night_0_en",0,],
-["features.ftue.impl.welcome_WelcomeView_Day_0_en","features.ftue.impl.welcome_WelcomeView_Night_0_en",20056,],
+["features.ftue.impl.welcome_WelcomeView_Day_0_en","features.ftue.impl.welcome_WelcomeView_Night_0_en",20070,],
["libraries.designsystem.ruler_WithRulers_Day_0_en","libraries.designsystem.ruler_WithRulers_Night_0_en",0,],
];
diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt
index 8d260821576..12352233377 100644
--- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt
+++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt
@@ -128,6 +128,7 @@ class KonsistPreviewTest {
"TimelineVideoWithCaptionRowPreview",
"TimelineViewMessageShieldPreview",
"UserAvatarColorsPreview",
+ "VoiceItemViewPlayPreview",
)
.assertTrue(
additionalMessage = "Functions for Preview should be named like this: Preview. " +
diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts
index ce9698ab3e3..7d9fa12efcf 100644
--- a/tests/testutils/build.gradle.kts
+++ b/tests/testutils/build.gradle.kts
@@ -24,6 +24,7 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.uiStrings)
+ implementation(projects.services.toolbox.api)
implementation(libs.test.turbine)
implementation(libs.molecule.runtime)
implementation(libs.androidx.compose.ui.test.junit)
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt
new file mode 100644
index 00000000000..fa60e497cdc
--- /dev/null
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.tests.testutils
+
+import androidx.test.platform.app.InstrumentationRegistry
+import io.element.android.services.toolbox.api.strings.StringProvider
+
+class InstrumentationStringProvider : StringProvider {
+ private val resource = InstrumentationRegistry.getInstrumentation().context.resources
+ override fun getString(resId: Int): String {
+ return resource.getString(resId)
+ }
+
+ override fun getString(resId: Int, vararg formatArgs: Any?): String {
+ return resource.getString(resId, *formatArgs)
+ }
+
+ override fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any?): String {
+ return resource.getQuantityString(resId, quantity, *formatArgs)
+ }
+}
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en.png
new file mode 100644
index 00000000000..6866b6afc06
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:74f93deb90501b746d95e0edf2ae2cef58036a388888e42a9b0fd8aadac9758c
+size 29447
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en.png
new file mode 100644
index 00000000000..92982109273
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6a9a248862bd05327e10481f868c6dc10cc1ccad6a56284d497a9fb45f737206
+size 35042
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en.png
new file mode 100644
index 00000000000..a5e66c073fb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef72766061f056096fc6d030b0e12d05dfa767ea5cac45d71b02cdbd0e78971b
+size 17859
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en.png
new file mode 100644
index 00000000000..9dda5c0f624
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e21673def1a4f03dfbc6c451299ffa993acf82b63d8de13400519a0e54625fa9
+size 18736
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en.png
new file mode 100644
index 00000000000..8eef4152845
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b98dc51e1f31bcd64dee223be7beb66b6ddd814189f290876633f5727d09f18
+size 27383
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en.png
new file mode 100644
index 00000000000..462b3bc35e6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a386397f9a0f037050797980a3bd9482ec8a2f1a37d5e150ed8cdd50e5b576a1
+size 31873
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en.png
new file mode 100644
index 00000000000..ab44725d4cb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b0d57fed7af1716e23f9f1167f70199f5a21f60f3ac97940deccd824754a901
+size 39156
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en.png
new file mode 100644
index 00000000000..29ea2f77896
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1a7c6898ea6667e5b7f09ee99ca6b2d75ae3bc20e0db57b9d7a20c48d4681dca
+size 27308
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en.png
new file mode 100644
index 00000000000..43684248f2e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e229ac5d069412a18ae7deeefd0e88d9897b239ab32a0edd2c2d06f139f1f1af
+size 32430
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en.png
new file mode 100644
index 00000000000..31c1d7e2209
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:62cf8a40ea8670ce9a23923d5791137ec057b130cde637c4dfb5b0edc75d7971
+size 15926
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en.png
new file mode 100644
index 00000000000..c34f208e494
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:903a894c80772fb4966542a6832d9d311c715495edd64bb5ca048f8218aea66a
+size 16944
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en.png
new file mode 100644
index 00000000000..02fd5af4b35
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:89fdc1cf49c756e40d81eb858b507f751a6ff661fcb4f3194a5d7c0f738c1b94
+size 25250
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png
new file mode 100644
index 00000000000..6abdd712c03
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ad9532960a251f04ec0fd513df561010a91923ab41de8318f99df23d3799feec
+size 28232
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en.png
new file mode 100644
index 00000000000..de9b3caf77f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8f2419c455efb74f0f528017e411c16d76755646e3449ae4a62077857e3ea93b
+size 36848
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_0_en.png
new file mode 100644
index 00000000000..8b36ceefb52
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0132466eb104c2249958a310b19336f97a5485f98e47a233c29740b14b9907ee
+size 14298
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_10_en.png
new file mode 100644
index 00000000000..8f0414d4b34
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_10_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9afb58cfcd13c064f0b17d449318d5111aaa914cb8875fe0596c66e5a8a17051
+size 30647
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_1_en.png
new file mode 100644
index 00000000000..2548547d60b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a67bb9ba78ea1d8802ce503e7d6a16be2ff46415df0e435efd7a36f3ee711b0d
+size 26453
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_2_en.png
new file mode 100644
index 00000000000..b75df7ecead
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a71bcfe210fdc6b2a0c54fe187d9528811bd6b5d442a66a1151b7a00ae514273
+size 33141
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_3_en.png
new file mode 100644
index 00000000000..92f98168bbc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:036a4f8f2f478df0e1705ff4bfd04599f6c0251936d2a80d750c9055d9d63ae0
+size 41550
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_4_en.png
new file mode 100644
index 00000000000..f125ca034eb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:99f070f9b5bdea3aa057776b303be171daff36f684770174f46b37ecc0eea781
+size 52135
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png
new file mode 100644
index 00000000000..9463263a2fd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dcd89988740d57132796306568378471b67ceb7c182e82fce5fa01dc936a640f
+size 44821
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_6_en.png
new file mode 100644
index 00000000000..a8e76f0ab74
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7ee73fabc6f4db8b7fb26b606723c69cf169e3a003a828b74654b6286235e164
+size 34430
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_7_en.png
new file mode 100644
index 00000000000..c4e4262ce24
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_7_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:170e1d045120d172d26d5d7bdd6bbf42c1a5bddcad7ff38d5352cd38c05efed4
+size 39551
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_8_en.png
new file mode 100644
index 00000000000..2952c5bd818
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_8_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1b71dc6dc0d29801e81d1d4e52df811624583f69c0510f2f650676747a326455
+size 30296
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_9_en.png
new file mode 100644
index 00000000000..9c47af83a47
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_9_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f6be2cfe7991bce6c622f4f2e45f4ac96810cdc9462d9a4d7aea11baf69ab5ac
+size 27446
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_0_en.png
new file mode 100644
index 00000000000..5ea469c53f3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ab53aa77207ee6984eb5a7a619b5b748178819bd2006ace4282f28f1c6b16cac
+size 13944
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_10_en.png
new file mode 100644
index 00000000000..54a5fc7d9c6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_10_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:68294db4cbd8bb42f4a0b6534b48585341beb2bc405e93cf74a2655f049522ff
+size 29768
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_1_en.png
new file mode 100644
index 00000000000..613fccf45cb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:be2ae50ce1b0e23fec7bcbbe9ca0f0381af4d0f29f1e7498be837d9d53b75542
+size 26003
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_2_en.png
new file mode 100644
index 00000000000..b9982135b9f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7548a1029b4e63722aca769800728c0bb519d3d736cb54138bc5e053a1aca46e
+size 32150
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png
new file mode 100644
index 00000000000..2c6855f6abb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b094584e67852da5ecb74511143943d3526965a548a24d5bb4e23540fc77b90
+size 40527
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_4_en.png
new file mode 100644
index 00000000000..0bca7905242
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:79e34ab35d9bd44d6e7694df882995f401f49a2ba042c6a3b2c010c12657750f
+size 50637
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png
new file mode 100644
index 00000000000..6fec3fbf9a0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:00c25643adf7f06ef46e824aa60b35348c18e596a82557b1546b55d29392aa11
+size 42613
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_6_en.png
new file mode 100644
index 00000000000..d6266123462
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ad43b5aaa3ea7cf3ab79af88a2a93ba4294527ad2c40c72f4312f3e1b92b4f1a
+size 33280
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_7_en.png
new file mode 100644
index 00000000000..3cbbf256eca
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_7_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6d9f612c2e6d46ed59ec5a3d1c65fe8953e4fc8f0955bddaa2abef6e1eabb00d
+size 37194
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_8_en.png
new file mode 100644
index 00000000000..215cef276c3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_8_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b67c8d2cedb724ece75a2171463c4e035d562c4480d39b11b1072f4fdae3053
+size 29840
diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_9_en.png
new file mode 100644
index 00000000000..171b5f5aa54
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_9_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15903c9494100becc4dd2bc6e9fb7f38f8eebbb595cbc251cfc0f839d9a99a27
+size 27322
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png
index a66e7fcd958..a967566289d 100644
--- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dcc448310dd9e586df3d8e27b9384e5047f9ebca04c1adf7be4d6d1a6ec88aa7
-size 28735
+oid sha256:8f4a7102b45fc1acd7c8cba59282548ae36bf8a3e65acc12c4041990f0cc61c0
+size 30165
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_3_en.png
new file mode 100644
index 00000000000..683799c4949
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7b16d4f9226d4df9efa9b57a0f6573e9fd6b0301f2e4cde2794de2863cf31ac5
+size 31373
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png
index 9f5c57aaeaa..ad9bc11f602 100644
--- a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2303d25aa0330a27c53d5349b26f86d965fdd87f64c118b6ec8c72a75aa49de7
-size 28165
+oid sha256:a5ccee59f5f0fbc79d252886b647003ce9b3e3b7cb8e54f5c154c37221e30c14
+size 29382
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_3_en.png
new file mode 100644
index 00000000000..a47858aeb1b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7afe56fbb521c23252ca5b375a594824d75bf80d9571f830acc14b0f9230d944
+size 30549
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png
index 0e0342e192a..3bf8f0bd3d7 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e67a540966100272311381e87011149cdb15c8191a6f2bbc40d1febff999c431
-size 24067
+oid sha256:4bcf877df431dfe4cf3c7f19d58c413356cdf77ae26f9dfbc9f9136fea3f5c02
+size 29361
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png
index f500ff60dd2..da9d93301b9 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:65023f7233f112547ccd0850c321b2d6610cfb6a1371c494f68722e75874f871
-size 45915
+oid sha256:88d5893c849bfb7945535f1f450e0c8b8975b5b4eb3ca336bff73aa607f2f34d
+size 48171
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png
index c80b8639e0b..e0975da1a9d 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e2c2e01bd133c7b84e381141b6baf22783cb585cb3af9f790785dbf8aeaeeed6
-size 47644
+oid sha256:3f0451276061db5c189062b32414fd93c2f174c3e7597ab723c3f1a0abbfae28
+size 49821
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png
index 4088be4e1e2..6b36ec50596 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9bd6fb63059cebc7dc7b850b5aa73549ab9846eda68b1b6d9a4f8e0716d42c3c
-size 40419
+oid sha256:f8ec0f80c44ea01158f58e53c0aeafe6df0586e13ff452a61c8b6a9ffb070702
+size 42824
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png
index a7981dc0a60..b5c92a79354 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9b54b8389ad5fc61e6aba9fb8d9a1e7beeb2ae9ee3090c5cf8221ae63ab7c0bd
-size 43447
+oid sha256:a453961a0bf17bf0219245c49f527567a207b78f46cbb9f1ea477d4db96b5c06
+size 45554
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png
index 87c07faab3c..2f90f049140 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1a2bc7e9098301d17e72a61a7baf01e52aa11566e1da4ffe3b43a66fa37652b2
-size 42103
+oid sha256:1a82048429ed60a4b246c37bc1b104e166c8c2402884766a9e487ca98ad45a57
+size 44391
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png
index 25da7b8bf1f..70d0754dabe 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f097af2773ff2ecf70a69e4292dc118b4c8616f1c8f979e7d121e86a16ef3072
-size 38506
+oid sha256:b7e8e9782859caf125c6258a612d52532cf005d1d21146e21068882b586f5d1d
+size 40788
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png
index 2bf210d5907..8e3e0660c19 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8f49870a5333cbaf1c6c3ac5ddcb85290d04efc79ffec93d8dc39b59f9712820
-size 42391
+oid sha256:640a1d04239f041ae395b56eee251a2942764c97dc50db3a8d4c3bfc44225a1a
+size 44670
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png
index 6de43e00eed..d1cba8905f6 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:883f160afa3fc3d2c0be5098205ea616875084ee0f122ebf994d0fee53a8ecd1
-size 39847
+oid sha256:150a3d6bd18e87b15575f7f452b222f07e6c3ef227032cbfe84a7728d17c491e
+size 42088
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png
index cf03f3c12f4..cf5c641ec78 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7790c892daeee8910ce17caac2c957d09fffec0d41a51b3adc1d5bef8dcee1a1
-size 42261
+oid sha256:737959115454525aa41c67d3ff47aa1829d78c7d18648b7c14b5e62ff6e44436
+size 44511
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png
index 63da19cb016..9778b3513b5 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bc7b733680a0d86ee1231345e58641fb3a208da21de62a50f624d9ae04c6e140
-size 31661
+oid sha256:dd031f67683678815eb3f4b55f3ee65fb79096035705019770a626bfc5c00794
+size 34009
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png
index 7e43e760b28..c281247aeb0 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:20504466241e36817557812f6b378aceab9c2e271a596bd3d037c6be41af7c54
-size 23624
+oid sha256:97fbbf4f3b9466a086a8824f9230a1ea5525314d818e19879a2c5340c9495138
+size 28642
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png
index 1667fe5ff02..c275736aa02 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:48028e4c3ca7e9b871683165f029430bcd4e0fc2411e4ffc83a93abb641d96a2
-size 45132
+oid sha256:69f31cbb83a84e57e8a9e7bf4ff451d5a192523486c400abdc293a467ca0ffa5
+size 47200
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png
index e44f3240152..17714bdf54c 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b040fe47521f5d9d76d7892412bb2b5cf7e2b93b951f5de5f772503797fb6b24
-size 46548
+oid sha256:410450770889b906c279084668980b41357e432345bc098560d8b06d6e35c86d
+size 48841
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png
index 5797dc53458..674ba67cce3 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6db046a97f14f2df3db2415d71a02ffd6e706fe73def67c21f0d844be279da59
-size 39617
+oid sha256:4ce4a7fdb57fd4afae868aa733b013fd5e4daea6aaa736256ed72d9d5eb759e7
+size 41988
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png
index 31fd8da4e8e..dc0370378dc 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:abe8262b81a5829538f9946060ed488c68876ebb77ee2d46490a0b2910d3dab8
-size 42597
+oid sha256:8392567a41faa52cd89dd51c9541a95e6898bd5d52cdf3f8c97986f8c28fea7c
+size 44738
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png
index 6d0c2a905bf..2e828a00058 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b88e54ba4743e1e245b5a4c7d6b4045f816241f6cfc8716b8cede0b62666222b
-size 41286
+oid sha256:ac363ebe575582b39b3ebd7857ff891994f512064f167fee342a9a2b2a35dd1e
+size 43572
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png
index c479ebef749..084646ff715 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d88b4d82bb0d416aff7b32647479181ac9702c36c08370e7671d6e5ef80687c7
-size 37825
+oid sha256:3d30a38861e317010616e7b17614803439f345e376f36b32e3e6e832070ada71
+size 40150
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png
index 885d6afaedd..133a8cbd14b 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b3d4dd1766a3f46acd86bdd5ec920c56a5e5f897c81c1477df10ba59dcd3d5b7
-size 41554
+oid sha256:96f4023e2a502fb50dc9c03ad53faa932baf1e316652651e109f21f531adb5de
+size 43819
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png
index 13b4a76016a..a6e70d1e6e8 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:842fbea74e53c8804fd0a3e9fe96dcb21b5c91a099e1de33d8ec983fd9a8a80a
-size 39112
+oid sha256:8f52f074d2dbf93c3170c80dd18f4601ad1e2db6c57773d0e10c3f8c29ffdc9f
+size 41303
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png
index 18e022f292c..5d6104964e6 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1311ec5e008b44d81e6556164f780c57dea3e4721cc47caccdbf337dd4027eb7
-size 41402
+oid sha256:504ac65ffbcb0705f3e73d32970cd345d23d19e4f190420f8953e55250056329
+size 43641
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png
index 99c6f765a55..720075f0075 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d2cf27d2f053f33c67d944a8f45e0206165b2db2f3b959eb9e0bb943f84fe6a1
-size 30657
+oid sha256:f72f6ec83324b0b6e3719e1e0f59387cb0376149decbb780970dcb7f219a1551
+size 33053
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_0_en.png
index 71d0679471d..acdab75b0d2 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:17aa655ef22beec5cef50f5d44cb2ecd8ad3a1979bc072a33079a49a8c4495c5
-size 397284
+oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
+size 395102
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png
index 9df61f791ef..16f3005b0b1 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8a0e722772be6b9d38c5f0ef3198502ca6e6ebb2599e3a4ebb4cc95cde907095
-size 52012
+oid sha256:26764da6595dee37219d5bbf3e53bad8db6451d7d39fa7741c61d96c89938bf8
+size 51246
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png
index 40926d90b9e..ccfd70d1bc7 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aa4cae0da657162117538b6839032ebf467542a043f18e308eeaf5ae28ca14e8
-size 51984
+oid sha256:b9d5ee3eb77075ca147fc7ed96c83a846dcfcf8e09bc129a24daa5ed19f262a7
+size 51217
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png
index 63c68481974..26f522cfa69 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:df6fc9bcb3636549fdfeacb4c9e19cc3c699b7519558bd14f409e7b0c6a84d80
-size 89846
+oid sha256:9f2882937b7dd90da5d1708185f0130b75659eed57eaf2a966280a891f4ddbd6
+size 89083
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png
index a31bc036b59..2ddab5fe37c 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aedd5d3a01457b8ed06b16cd63d08f24ae34568e2b912b82fb892b154c94e6df
-size 392780
+oid sha256:7af7dedec8de4ac367fc870d2c235060b3a57d27b2bb879dc784a3cef3c2a370
+size 390628
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png
index 71d0679471d..acdab75b0d2 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview_AttachmentsView_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:17aa655ef22beec5cef50f5d44cb2ecd8ad3a1979bc072a33079a49a8c4495c5
-size 397284
+oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
+size 395102
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en.png
index 8f51f4d170e..7fec52751b0 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aba63b0f223f8480d40e6c9c48ce44a1ae1a8bdcc3d101030b9a0bd5f1e9ebee
-size 23773
+oid sha256:dc3b043dcc28ab54ea0564e355b56d39ff79acd5180c9a34f727cb6e113b2611
+size 14473
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en.png
index cff8f7e35f8..8f51f4d170e 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d5c8494ebb4ceaf3a31b661aec47f8a33afeb7aab1457483b7009099a6b56f86
-size 8960
+oid sha256:aba63b0f223f8480d40e6c9c48ce44a1ae1a8bdcc3d101030b9a0bd5f1e9ebee
+size 23773
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en.png
index afa1dd9b61b..cff8f7e35f8 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fee2af8462597d58274b7168c5cfe1f6d6b0909b048adc0f4fc5b3c12a90b859
-size 8861
+oid sha256:d5c8494ebb4ceaf3a31b661aec47f8a33afeb7aab1457483b7009099a6b56f86
+size 8960
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en.png
new file mode 100644
index 00000000000..afa1dd9b61b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Day_8_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fee2af8462597d58274b7168c5cfe1f6d6b0909b048adc0f4fc5b3c12a90b859
+size 8861
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en.png
index 8561e181513..3ab5d1cac87 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5d0a687a259fafe830560b762a915e478d680dd7c34a295d72e82fc7c427a815
-size 23403
+oid sha256:c857b7836bc9369cfc950b774479dafcb717d6acc40cf7d8b9b34b99974573db
+size 14377
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en.png
index a851ce84e55..8561e181513 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dda55a19381d9f51a270afa644894f909d8c479be0a5e57b977393c9f1253683
-size 8960
+oid sha256:5d0a687a259fafe830560b762a915e478d680dd7c34a295d72e82fc7c427a815
+size 23403
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en.png
index 212726df377..a851ce84e55 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d4f48cd7dc7e2bbf7363d1127e46721c25bb8dd887927dbcffe525f5bb5bae01
-size 8781
+oid sha256:dda55a19381d9f51a270afa644894f909d8c479be0a5e57b977393c9f1253683
+size 8960
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en.png
new file mode 100644
index 00000000000..212726df377
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemEncryptedView_Night_8_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d4f48cd7dc7e2bbf7363d1127e46721c25bb8dd887927dbcffe525f5bb5bae01
+size 8781
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png
index 3b66fd9f8e7..4cdade6e95e 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad01f084328ff5c31019c00bfdda18660f464164f68a9f92375afb7854dda7fa
-size 59491
+oid sha256:d3f2c9de8eeafe23994d98605942b910135e5be357cf16fe61adcac88d5f25a8
+size 57402
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png
index 2c917a94f21..5c905c60bed 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7d1ed9a282d3363fa4e28d924a750d0a46f7564533f687ab102d559e2dd1606b
-size 59518
+oid sha256:fc48b63a1e4e91a7d91e19fae3a7f1d419117b94b330b1f1ca2ec0761840fb99
+size 57430
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png
index 2f6c08ed16e..c2b9c93351a 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9660c7ea6ca5101c6b5fa9e774d4063d4011b9f8fdb64bf8e32cbc89c231531c
-size 62605
+oid sha256:0f749ce126bafb5f3a3027cc1ce06897faddb6a052175956d3e6c7459c2222ed
+size 60503
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png
index c7c8f2d132c..fcdbb89b2be 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:31856cb9bed162d0bfa6d9b72a693cdd1fce231130b6f031ef7212b11068c6eb
-size 58650
+oid sha256:109e604b9584e18f1955f9824c43673cef227aba704fdc98b86e2798ca29dabc
+size 56561
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png
index dbb59f9ad38..970293d9f2c 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:097625b9d81cc5a6f426a5a50292848f4ca7c282feca650a08a520380dc9ec95
-size 55590
+oid sha256:a1a409e7a5371dd3452985c50ed67172f03fd9e7ff19a0c673571c6d80f649c8
+size 54891
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png
index 74460ddcae0..9cfd8d77af3 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:53e8bb8dc1e9de5d02ed59a9d9384cb02394eb7e509ee1d30a718231fd82f08c
-size 57292
+oid sha256:bc1755d89b6ee527e81aab0fba75f7642f5b0932be2d61b0202bd6bcd5f60d5a
+size 55193
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png
index 5b32eb70815..572698b4959 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:78106e4780daf39fc8791f502cf28a36c372fce08e2ee2c8959dccd58c2587aa
-size 58576
+oid sha256:cc5472829d4a004f4fd20a87ff557043cb3d0dc7eca7a6505120a73d8bed2919
+size 57863
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png
index 5779f0e9b36..01c7b438a6e 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3ce4299e4c585abf3635b37c6003ac50a8ed25ffa0e9258dfbc07d61fb04569f
-size 61074
+oid sha256:d89ff85dd7a99faf50ddc167514c15f9f86f8c083e5acbf602183c01bf42db07
+size 58982
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png
index 67315b78fe7..9068937a1d8 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:035d793ee33682785b98974c6357b99bfb879f8a86cba7117bb2b9c4c4bc13d5
-size 49533
+oid sha256:e0b94405bad0e66c5323c643285d7659da07745d229671cca8507f3144e7d6af
+size 48819
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png
index 7300f46f29b..164a23e8617 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d40877679c1b9cff47940631f237243d3d9b715e50f44591d2f7588279c9052c
-size 59176
+oid sha256:5ca213558819b9265e82d3b22703bf1ea798e7bd8064b7d8694f4ee56ba40d0d
+size 57020
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png
index eee7da7dda4..db0b54c5742 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:76faea3ec5b9e01a7e1ad77dea4f44b060bee26cf1d0b56df8eabb692cf764df
-size 59225
+oid sha256:2c58e77b63f813bae7784a29bdedc19bb067b2f736b1c228bb146ca8e8f46756
+size 57068
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png
index 8643a775a14..330b0f53173 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0f9df03bc6cba3fbf8ae454768064c9141b05e319c40c568a0a3edf3a53fed4e
-size 61961
+oid sha256:3f4319d951caafd65c7f8bb574e450465e87a55d5fcf2c4f42177cc1494f8b92
+size 59771
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png
index af5c8df13a0..51b4ed5c882 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:952e2bbcee90fb03f522da1850112b88496b5851bbc0c9908505fe371d0bd34c
-size 58047
+oid sha256:dbc4b083658c24dd574bafd3e66ed9298622f6c5ad0ee726c24c1bca6294217f
+size 55881
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png
index dd5acfbb267..4c4650aff2c 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fba26f4a18cd5ea132275b9edf499b51a1c7ef3ecd1c61f30768ef3236cfaa06
-size 51723
+oid sha256:ffb03682ef773068b6ef997e94a3c4424d0ec504ea2b4afcf39a773094ad4598
+size 50920
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png
index 7c7b6520892..b377553a6dd 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8f1d5a9f286af43a8d026a62a7059c221cfa824217f362c9daeb9b0552fc7bb5
-size 56907
+oid sha256:1e29e68366a29aab72a9e5e79e20e1a02e5869974cc63cfe99365bdf31e413f1
+size 54784
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png
index d9e9f4e6a23..f8cc462343e 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e6af2e7c054eaaf6c239ef862adac19c7f821198d5ac08325d5ef15f725e5b21
-size 54397
+oid sha256:3fcd068f1658db46ac182ad8ca93db6886357373ca936ab2795ae9b5f35af3d0
+size 53602
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png
index 39c5faa5429..1fa6a1784d8 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7e9412f3e84b63be532188e13a976d1954404466136633f3f8cd41851617e753
-size 60668
+oid sha256:38ee3e41f6258befb1970113c5f638353b3f9c67a34fba581108cf92b5151e65
+size 58500
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png
index df384039ee1..082ba4881ab 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1da55e4b116cbf0ebd74afee2a47d34b99d87192cdf9a5f8698118fcd4bbe239
-size 45466
+oid sha256:e5d4696e080d7be0311b8d29a01d1bff3fd4291410734cd3d4cabaab4d5a411c
+size 44672
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png
index 53d8352e7bf..1d6c1410737 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5919175f0e9712e5d7f27487555cae2e46bc99077ba5015813d789440a1af06d
-size 35736
+oid sha256:d0e7a1e100bc43909382074360b4e4fc989c5830295754fa3796794c73d8d9d6
+size 35734
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png
index 7c95cd7a95d..8fb27d8f45a 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:13533c1822c2b07bc0a82f1c0a0a2898577294c29428976fb8ff2e1d9cd1ab75
-size 35892
+oid sha256:988c4697cc019532f0200bcd588dee74924a0cdd5d4add11a83ae040a195dcab
+size 35890
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png
index efe317b36fd..8dc372dbd7d 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1d6b9c4c3c3d838421b91d0acf6ab826417d5ada7058a9b7dfc36fe54fc435d7
+oid sha256:c7b508f76f2f2b7680b8ad39eca330d352e205e3ddfcb9a6ceaff0a3c365d7b3
size 36588
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png
index 504ff2e44ac..71a0a88d64f 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewLight_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e3dbdb5145340166122680c2883cbdd83c6616e76089c4af70c80da25edf6aad
-size 36840
+oid sha256:3edcb57534714a66b5d4d532654af3a41e72d0f0015e3272b24178199f583db6
+size 36839
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png
index 76446e61801..12b31321182 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:94f040a3d18493f80b5f90eb48e68c664de5ddee0ae4575905ce35709d31abe9
-size 40969
+oid sha256:c7828106cd3724769c5bbeaad50c3417264abcb6af40ffd90aee283e8b29e579
+size 41831
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png
index 52912aa0fd8..58d7c45b3de 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:96cf72bdceae29a86593ed3bd02d5edbe1f5422e5be0798f536b49805088b0b8
-size 45109
+oid sha256:f9c24abc59ed8ef26f647b5d3855b768af6043bdbb035f0d2756a7b783f64561
+size 39848
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png
index 0ae9c7dc0e6..e2e7745abc0 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0c719ba2c0782ecf8ebf37c07dbc79d37b1d993e4987388ceafbefb31b03d100
-size 44064
+oid sha256:b87d165479dd2a0d6497fdac37bb43de760fd0eade06ad23b53baff667b8af03
+size 38783
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png
index fba01a25c62..09080780f9c 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_12_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a675afee3fcef0f8468ec93e33e1e86398bda517f4f54615aaf527d549387431
-size 47217
+oid sha256:2e7726872c78a2bcddfefa689699d7bf69a09b55c023bbc7008575fbec5b7779
+size 41986
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png
index b3273a0efd4..62a3a99fba1 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_13_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ced35352da8f7b6c9d4a5647cf1ff29f194d4f68ca9eec9c268ab889271e4776
-size 45507
+oid sha256:6235b9e9b6ffb9e4d813cfa49c9819a3cbc112d6dc5d25d7901a257e4353e609
+size 40241
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png
index 4a0208786f0..4f70fd407b6 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3444cc70e1f1b212d89ba404199a439a498281aa9faff9a9bb2469b727498224
-size 37486
+oid sha256:75bcd07324f5acb45552d8d5bbe369cece798d109f3c096859c6a88fccc8e2e1
+size 39797
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png
index 16b29959610..5591d4b2515 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:afcf1a235cd16b501ec02f7da90cf4800df41ab07383b7e6ed502f4e9249855e
-size 38354
+oid sha256:f69da1fef91846329a0835dfc2be82b1f49892193dadf139f6ac262355aff86e
+size 39910
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png
index cedbb0f72d1..406387611a7 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5338fba98c85142b4300467a564e8920627ba83ec91dffb7e2370d07447b8d78
-size 38479
+oid sha256:2ddb13a08e77486addc983662966128492074b5a2ddc4afb193ba459c8366952
+size 40544
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png
index f7c16b996f3..ac33cc95436 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9c7887b5f1cc07ef30ef347c149af51edbc1c4539615c04fef57737839677423
-size 44293
+oid sha256:003386cc6af1fb6c3d52724e234627021a131f6eaf4c5261f58bfbba9d87bb54
+size 40981
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png
index 374f7404f14..39309900996 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bbdec0e6f569c4d1646545b7cc03317596cb43aa4faef9a93e646dacd2185aac
-size 40277
+oid sha256:6e1b04ecd3685f654310da8f265b55bfdb72b56e6d3802817b9b591993dc0719
+size 41594
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png
index 5bcef70e539..f36bde9aec2 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a83a962d8252f993381effcbe5d52b117ce4850bc29cd6e04f138426b8c68510
-size 36416
+oid sha256:c88a52f6765d672862cd6baad51f3d9ecc2d87f21a8cd6835ed62a4228bbd52a
+size 38445
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png
index 52789eb061f..4bc9febcfda 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ae95b22ef977a95f9de6dba9e8ed2b33ebca808db4437512be39f020fd8831da
-size 46411
+oid sha256:0b2d15ec833e6e20fe302d377c11f686275d5d115dbba070d623c46f66f95f95
+size 41111
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png
index aa849d237d0..4aaa0ce01a1 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2724c07c5ce097c2c470ecb4168fe2d101fb56a2b37a88065aa9210b14b871be
-size 45403
+oid sha256:0f8a39cfe4c761462b84425e1008a5c36c8f93a6d00e7e6f2ece108575bf6b89
+size 40124
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png
index c4aca87a180..3a7de203757 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetailsDark_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:df340c45b1d0a07f53adcc2872aef1a691c4fe4d0280e961524281a3dc1e427b
-size 45412
+oid sha256:959b422fefa73279a7a3ef2193c7cd5c597fbedbaddea7de245cc86b820c2514
+size 40158
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png
index a6694758f1f..8e8c5d5fcd7 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bee1a47e22df24ba29b46dac1c8c2da8c38b3d438f8ef5b72dc3c39b0900338a
-size 41908
+oid sha256:da5e1b0b8dc2663baf0e491878868f43c078d6be0d81d4776dacbd28f34d794c
+size 42866
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png
index c09b745e243..11c6879648f 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:655995891afd39283b5271280d594d6b2ca0e3ff004e81ebbc4e351be0cd185e
-size 46007
+oid sha256:0837ae930f448d0cf0f0614c437ead15398cafa5a96aa50e0967cf297d3ff355
+size 40662
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png
index c3ddd6dd928..2485aea5d37 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ff42edd6e8f1165bfe8ad24ad2e1a37a34138b30193283ceb070e09273c37247
-size 44976
+oid sha256:72b557a35f41804729912f653ff64a70c9173aeff63471367bf4f5e88bd8e7b8
+size 39664
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png
index ea86ba1b2e5..c6b95729a7f 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_12_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a21c4945fa617d0bdd5549c98a0b18067f302cb71e7e012728a61120a6ba7269
-size 47772
+oid sha256:684bae4170ee939c3317880889fa28e4c467ddf704a13280ca9a1e33d9ac7776
+size 42526
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png
index b4d73c57b74..72c9a2497cb 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_13_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dc76558ad62b1d9ec77afd066b2c64edc7241b93aaa431d8545041f7024315b1
-size 46443
+oid sha256:2c96854d4054cec6a4e3f2f642def4abc500f9a22dfe1aadd24cf45eb326a815
+size 41109
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png
index fec1bbe8060..34156265b98 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ab627c807db2e5bcf339f01fd0f11f5e79529b32165d96f2a192faa863c38dd4
-size 38380
+oid sha256:f47468b9b9dc5c7c91b2d0b1446fd3f6e45ea9ee5b760029421a28e52410db77
+size 40935
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png
index 7f4fb4df944..17e997b9198 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:af7f4944f2e1bd57e1a02716ff02a67beeb8a05847a0d06387fa0a8ca5ec0481
-size 39221
+oid sha256:0ff7f7f25df81af089c0c22bb7937f01ea1d05e7acc09de1fd8c355f03c329c7
+size 41035
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png
index fe253a9a6e4..cdf150fbc07 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:647a6e7c0fdbb3d89aa411c14a2f41b127dd597fa157f4ef37a909132368c47e
-size 39114
+oid sha256:b6259c338f58959fc732676553418f8b6dfd5e50cd3b55ceac75b5d95a9feda5
+size 41323
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png
index d0bb2ef17ca..5c7a431cbd2 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c0ea6b1bf786b06fbe4ad211f9dbda7094f30f5089f7bb186d4024f064612785
-size 45210
+oid sha256:2fde798527f42790e6fe230dc896481223db0481605f551e5f7e840a3a78566b
+size 42028
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png
index 13ff0bc7cd4..40d20528c45 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c8eb77b73498f0489fab354b96d1676cd8f82e5fed175c600dafcdc126f606eb
-size 41115
+oid sha256:b68a248d99124a81d1ce382510a52f12270a2b2917cec9dba349dbd8751c99e7
+size 42478
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png
index ff80d880b2d..f94d678c895 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c28a704adde147d4523691dfe0decbbe839f1bbb92ecea4fcb146d378defaa82
-size 37465
+oid sha256:ef7656dc67b7060e51dcd7506502f60514b761b3a92e8aeffdd855e4628994cc
+size 39632
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png
index 8656744f17a..12816f97f35 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:526a16419357ee26e460511190d95c4f9adf8686d8a688704634025298b5153e
-size 47451
+oid sha256:3c1002160e799e3fb8c122f030f76dc29b0a766e533a9fe2151c9244777293f8
+size 42181
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png
index 34883fce291..3e61505a0e6 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fe2e1f53003df2f9fd33e90861221a4adec4e4104ddf1162502b70a895b798eb
-size 46410
+oid sha256:139702b064abb7b4b2d862e6f05730e2d3cc631cde28bde6c2a0770c5e08dbd1
+size 41072
diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png
index 5fa82cc6ceb..d4bea567729 100644
--- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl_RoomDetails_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:44e97087d7fefeb63beb81ef0e52ea6616820bf325217f75aa0a11806f6c4313
-size 46366
+oid sha256:a9c44b7df1330d879fb78c3cde3e4664a470fec1508183f274cb5c572d457993
+size 41033
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en.png
index 9939b2f5fc2..7d28a0e898f 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0bc6ca9fdca5e61bcbc490eda3faa9931212a1fcf8836b756c629d64f71dfb44
-size 16285
+oid sha256:6ed28ec4c611e0c6b01e4f9742bbd579593103859775d41118bb11fef0586b11
+size 16140
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en.png
index 64b1a49349a..0f84095ec70 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:515442ef3d33d93b172ce0db8b0f2b06be2eee7137933128cf414223d8161b33
-size 14044
+oid sha256:f3e9406dc7e490469cfd6178c66f12af16b89c68241b939db81bb160232a1c61
+size 13938
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en.png
index e3c391e6baf..5206ffd141a 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ea7920d1f69056b78471a4f789a02282d6929c72d542e4863affffa1cd99bbd1
-size 22611
+oid sha256:8bfa6e9b48e6ac6f5079127f6b978a5c144941d79ba347f91251ff0e6f1e3ae1
+size 21680
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en.png
index e3c391e6baf..5206ffd141a 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ea7920d1f69056b78471a4f789a02282d6929c72d542e4863affffa1cd99bbd1
-size 22611
+oid sha256:8bfa6e9b48e6ac6f5079127f6b978a5c144941d79ba347f91251ff0e6f1e3ae1
+size 21680
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en.png
index 1ea727091ec..f3c7ad95437 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4d53449b4f515a15207ed194469d56266f77c7208856c8481bba4bc89d681ec3
-size 16472
+oid sha256:6715956a1ffc033a907da275bd69397b423b7819346355933278f5f1187b20fa
+size 16380
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en.png
index 64b1a49349a..0f84095ec70 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:515442ef3d33d93b172ce0db8b0f2b06be2eee7137933128cf414223d8161b33
-size 14044
+oid sha256:f3e9406dc7e490469cfd6178c66f12af16b89c68241b939db81bb160232a1c61
+size 13938
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en.png
index e3c391e6baf..5206ffd141a 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ea7920d1f69056b78471a4f789a02282d6929c72d542e4863affffa1cd99bbd1
-size 22611
+oid sha256:8bfa6e9b48e6ac6f5079127f6b978a5c144941d79ba347f91251ff0e6f1e3ae1
+size 21680
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en.png
index e3c391e6baf..5206ffd141a 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Day_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ea7920d1f69056b78471a4f789a02282d6929c72d542e4863affffa1cd99bbd1
-size 22611
+oid sha256:8bfa6e9b48e6ac6f5079127f6b978a5c144941d79ba347f91251ff0e6f1e3ae1
+size 21680
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en.png
index ec9abfb9448..b37ea9738e9 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:484f2b3e592efbc3913cac20ce6cc7df3fe9424f0b40a16faaa870858756d847
-size 15750
+oid sha256:97a889bc71a579978cf4656a296221c4a929914f32dc0d74cd4faa07379ae519
+size 15487
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en.png
index 7eaaa4292be..9a235d85c52 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:33d7d1c4a7ff4e1fd82963969721b23326f7a86c4f716b877b5f65455c564902
-size 13572
+oid sha256:9122040db16a68daf4b88e0ef8239fcb28e1a858f6ab1bc0b352926ab7819caf
+size 13427
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en.png
index ad35d903c96..5eab82d5906 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92c11a80a52d977e27f560c7d9b80e87328ebf14406647c62c05a4c6c962e2d9
-size 21978
+oid sha256:39769a4a1de0cb8962fc14de88f8d4e8988ef36419fb18ebb5524f08f6eda001
+size 20811
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en.png
index ad35d903c96..5eab82d5906 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92c11a80a52d977e27f560c7d9b80e87328ebf14406647c62c05a4c6c962e2d9
-size 21978
+oid sha256:39769a4a1de0cb8962fc14de88f8d4e8988ef36419fb18ebb5524f08f6eda001
+size 20811
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en.png
index 2c27fddc9a4..b248cd5960f 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:597682e1dc18488401665fe4404fc98f4d76a6587cd142d9e41e826138affc32
-size 15961
+oid sha256:5f7f2901b56ac490efd3c0dc3d59f5fcffdb33f063e82060820bb0b352cba968
+size 15827
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en.png
index 7eaaa4292be..9a235d85c52 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:33d7d1c4a7ff4e1fd82963969721b23326f7a86c4f716b877b5f65455c564902
-size 13572
+oid sha256:9122040db16a68daf4b88e0ef8239fcb28e1a858f6ab1bc0b352926ab7819caf
+size 13427
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en.png
index ad35d903c96..5eab82d5906 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92c11a80a52d977e27f560c7d9b80e87328ebf14406647c62c05a4c6c962e2d9
-size 21978
+oid sha256:39769a4a1de0cb8962fc14de88f8d4e8988ef36419fb18ebb5524f08f6eda001
+size 20811
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en.png
index ad35d903c96..5eab82d5906 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup.views_RecoveryKeyView_Night_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92c11a80a52d977e27f560c7d9b80e87328ebf14406647c62c05a4c6c962e2d9
-size 21978
+oid sha256:39769a4a1de0cb8962fc14de88f8d4e8988ef36419fb18ebb5524f08f6eda001
+size 20811
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png
index 5f5470c52cb..ec2a7733634 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:226b0e80f8b0b39c0413bc72ad145b6edebb71f58bb1ad7d41dda49776b09f4d
-size 42643
+oid sha256:2299427647ef9673a68b5235bc3503007874d0570324a17d3c3ee40a5e581af6
+size 42671
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png
index 15916a2f9c5..7e94c40141f 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4ac77b3456739f0abae3d05b03b35a17ff7b7d7a625f1bebd9b36b6c759e7e7d
-size 40367
+oid sha256:0a011821df73e73ac3ec15caef2c6cff228cf0e5567271164b655aedbd4755bf
+size 40228
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png
index c8d4132209c..7cbb9c3fbe3 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3a4aefb49b16a1bed3547d1259e3159590ef6428a73687b8b4bfdc9a9eea45e5
-size 57599
+oid sha256:b71e77d68b21262c0dc0731a4bd4a62053ab94f5ae3ef7ac27b29154cfe7556e
+size 56493
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png
index c8d4132209c..7cbb9c3fbe3 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3a4aefb49b16a1bed3547d1259e3159590ef6428a73687b8b4bfdc9a9eea45e5
-size 57599
+oid sha256:b71e77d68b21262c0dc0731a4bd4a62053ab94f5ae3ef7ac27b29154cfe7556e
+size 56493
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png
index cb557befe2c..df1b3a35484 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:db5e61758f4decd9422be2ad11e595510cffb2433d42ec97923e4a7e3dc2610e
-size 51310
+oid sha256:222f29e2c22cc2381a0c533fb237d61cf6a3537ed3f00f29ec7931b9933d841f
+size 49987
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png
index 6dc2165b3dc..01d621ac7f7 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:459f4185a7c9eaaedd179fdfa117fd730238277995c6520a7e0eba084762a640
-size 41368
+oid sha256:1220f0136cf5dd3558dcd08ff7f820fac4395b4b35fd5b144faa65ecf01ff620
+size 41305
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png
index a62217d12e6..e69ddadd5a4 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:029a76515d629b66668f174ff55d5bc26be19e1a2d708f282b2de6213b1575cf
-size 39090
+oid sha256:b538d064bf3efda30bc981e24390e65918e3ae570f13b1329bcc17e55e4216c7
+size 39020
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png
index ff6109ec98c..cceb41304b0 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b5b7a4b6d22c15e79ab8e547f10cf5298964d6de4ac5f6b10e0fd12380322350
-size 56012
+oid sha256:52cc26ca7c7db86be00075dcdd39ac364339c0b462edc529c21f17187fb878c2
+size 54642
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png
index ff6109ec98c..cceb41304b0 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b5b7a4b6d22c15e79ab8e547f10cf5298964d6de4ac5f6b10e0fd12380322350
-size 56012
+oid sha256:52cc26ca7c7db86be00075dcdd39ac364339c0b462edc529c21f17187fb878c2
+size 54642
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png
index 3c9ab417dbf..ae0e7d80786 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupViewChange_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d8a88518bc44f9554dbe9db6c86c51f26f54aff7172dfa1a011e8c10b0f8dac6
-size 48564
+oid sha256:a0cf9ffc7f76bd53c4b94d02f0507994972993ae50ce022db72542a283f780b5
+size 47115
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png
index bd4be5fcb33..3b466b3aa05 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aaf674ca4ba02eaaff9f07fdc6374c5b195e7b9c8eb2e03b875e2690a236a975
-size 44115
+oid sha256:86dcedd7d1fb23ba10eb8b3e1c5d7f71331334cb98fb88bfa2a1fef0fffb3783
+size 44053
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png
index 0cb67f883f7..5d0f2b097c4 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9a96a86966223eb3ae22be485d8a7faf7607f0f9de8462419ad13772c55a8a76
-size 41939
+oid sha256:bcdb036e2a7581e69d3b658d16bb26e98aa27156fbafd17e9da15fd103158299
+size 41819
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png
index c8d4132209c..7cbb9c3fbe3 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3a4aefb49b16a1bed3547d1259e3159590ef6428a73687b8b4bfdc9a9eea45e5
-size 57599
+oid sha256:b71e77d68b21262c0dc0731a4bd4a62053ab94f5ae3ef7ac27b29154cfe7556e
+size 56493
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png
index c8d4132209c..7cbb9c3fbe3 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3a4aefb49b16a1bed3547d1259e3159590ef6428a73687b8b4bfdc9a9eea45e5
-size 57599
+oid sha256:b71e77d68b21262c0dc0731a4bd4a62053ab94f5ae3ef7ac27b29154cfe7556e
+size 56493
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png
index cb557befe2c..df1b3a35484 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:db5e61758f4decd9422be2ad11e595510cffb2433d42ec97923e4a7e3dc2610e
-size 51310
+oid sha256:222f29e2c22cc2381a0c533fb237d61cf6a3537ed3f00f29ec7931b9933d841f
+size 49987
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png
index 4c5e8940c8f..cd6dda7370c 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:19b68c0545f4b087d19f5f54f3bb104077dcde4622cd5a51803c34ff8111db67
-size 42837
+oid sha256:64777bafec80f98b0b1f9b7e05ede31036032b97808d52911ddd9045d83dae58
+size 42794
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png
index af5a72d8020..d6a004275e9 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b1fae355b34c7b34026bbf932f3e6ba44483e8c39d2d1d3de45f638fd370024f
-size 40729
+oid sha256:0d98ab21d3ae8099bd4f458c6422cf8d91a7405b45b1a41cc91fef78ccaf9475
+size 40658
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png
index ff6109ec98c..cceb41304b0 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b5b7a4b6d22c15e79ab8e547f10cf5298964d6de4ac5f6b10e0fd12380322350
-size 56012
+oid sha256:52cc26ca7c7db86be00075dcdd39ac364339c0b462edc529c21f17187fb878c2
+size 54642
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png
index ff6109ec98c..cceb41304b0 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b5b7a4b6d22c15e79ab8e547f10cf5298964d6de4ac5f6b10e0fd12380322350
-size 56012
+oid sha256:52cc26ca7c7db86be00075dcdd39ac364339c0b462edc529c21f17187fb878c2
+size 54642
diff --git a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png
index 3c9ab417dbf..ae0e7d80786 100644
--- a/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.securebackup.impl.setup_SecureBackupSetupView_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d8a88518bc44f9554dbe9db6c86c51f26f54aff7172dfa1a011e8c10b0f8dac6
-size 48564
+oid sha256:a0cf9ffc7f76bd53c4b94d02f0507994972993ae50ce022db72542a283f780b5
+size 47115
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png
index a9f026f29ee..08f11079b88 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:57e9087377ae492b35c194b870b5fd39374ee6f13248bf768bf91decb9859536
-size 22884
+oid sha256:11305611c23b01fcd9dba5b89342c4860041a89f2a3516bdabee9c202781fae6
+size 25087
diff --git a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png
index 813747a76bc..a1cf77b9df5 100644
--- a/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.userprofile.shared_UserProfileView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:80b18151db35194260b9d69bbe0c4072f111eff8ee5647011a042d2b7f2dc0dc
-size 22257
+oid sha256:0e5212de94c198e9daaee14cafa990bfbb0f9409dca7c45c9456221b1cc74a14
+size 24330
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png
new file mode 100644
index 00000000000..7c2a059e7ca
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d0506879a20bd64cb3a4ea41c93dfa78da1ca3b0c2728ce3044caa56f6648584
+size 105611
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png
new file mode 100644
index 00000000000..160c9d66fd9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e78aa3521464a53c000298dbd4ef51d4d0ea1d3c75b7bb8dcbd933701bab36b
+size 84060
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png
new file mode 100644
index 00000000000..36a6b1bf7ac
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9eec4fc7e72588f957cd7d741e29b8eaed71555fc0d66c43e6ae69cc64b924c
+size 87650
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png
new file mode 100644
index 00000000000..ef7b68df390
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d4ae4608296b5bb24c128572cc6b80379a9e3cd12ec89db9693c301e427f6ae5
+size 82330
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png
new file mode 100644
index 00000000000..7b48600104a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e125730c22fcbc843fc0445e0af06d48b7dc31896053b5a7341b6dde84526eb8
+size 82540
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_78_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_78_en.png
new file mode 100644
index 00000000000..cf85f766ab1
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_78_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:66c29b560708bb2d8870d8cf5caf4f0da49815b5527fed7294a88c0b3aa05c73
+size 18079
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_79_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_79_en.png
new file mode 100644
index 00000000000..b27ebae0ced
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_79_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3dd106bc9b54dfc0c4e3e9a5f04d3df52f58f71e4c3cd3d80f53d3f1308f22a1
+size 16838
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png
new file mode 100644
index 00000000000..1698660c8eb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4979794c700bc8bab1bc767cc2387ce3be28b9d2c0b6da0696b237445ce7df95
+size 21225
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_81_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_81_en.png
new file mode 100644
index 00000000000..3c5b96d217b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_81_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5abc97134f8a5f0d037367c9278d8f007543463e13c8e043241f292949681ff6
+size 17728
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_82_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_82_en.png
new file mode 100644
index 00000000000..11007374676
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_82_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:972653df5c62859c175a62d7809193afd0cb68832e3567760082f1b164e3424b
+size 16990
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png
new file mode 100644
index 00000000000..72c687d4395
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:491cb82462df122b0e8d21c5b70e8db8ac19c28aaee1289db6f4774e7d31d53f
+size 19556
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_84_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_84_en.png
new file mode 100644
index 00000000000..d78e8bad2a9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_84_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:938fb1c1ade57ac6421387d9ea5142448842a32d7ce446d4743d1aa15b2944d9
+size 15284
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_85_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_85_en.png
new file mode 100644
index 00000000000..deb69c6aa1e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_85_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a7619c6bc9d0cb6ea7660992ed16ce24eafe10052f9a8a6e7708f7bc5c079fdc
+size 14541
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png
new file mode 100644
index 00000000000..bd51d8c202c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4bef7c3d043454a4faf858456c7fcc98d1a17a0846a891af3332e76c4e10b553
+size 17102
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.media_WaveformPlaybackView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.media_WaveformPlaybackView_Day_0_en.png
index 3c573bace12..18fe30dd7ae 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.media_WaveformPlaybackView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.media_WaveformPlaybackView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bbe0bf5ff3c5128405fe0af2af344140a655d93826bf7591393dbd4732a7b729
-size 8383
+oid sha256:470ea6854c3786db7935c55a852637c907665326a61a5dcf33c66f0710406c09
+size 9650
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.media_WaveformPlaybackView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.media_WaveformPlaybackView_Night_0_en.png
index 2acb4b3d256..0ef108d30bf 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.media_WaveformPlaybackView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.media_WaveformPlaybackView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8ec9068b2f5b7bdf0bcf6ede6c2ea02040c1a77b1054c1fbf45fe5feb1ab78e5
-size 8147
+oid sha256:1b1ef50d42e57a1465de82d538a573c41a73a133890ecf84bed0317d38e33440
+size 9385
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png
index 261e330a8fa..0d92b4ca6dd 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ee6ae9f6af47e39480ec9e78d37da7d2f7174cba71c5274bb6314a2dc346b1ab
-size 10691
+oid sha256:6bed157ced6cb695c6c92f15bc8b31b124fb943f86db580c9ab2db33d423b731
+size 12408
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png
index c057b5283e2..36f2db136ed 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_BigIcon_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e0ee879f0cb6b6d42aa0706c1d2ce763211d95768e0111f03e79eaa923515534
-size 10615
+oid sha256:211c37c03a828d65c2d28622ebb7eee49d39941f80aca809698561c55ae12135
+size 12521
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_3_en.png
index d51d5098231..951f776d62a 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b0ae9f53b675f2f754f7cb3ebf3fbe45ae7eae0c63bc8628425e0bf21ff95bcb
-size 12485
+oid sha256:315e1d831a1e3082a9d4ea749b76b374b0f8018c6d5c11b1f48ed34db3b3a1c5
+size 13279
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_4_en.png
index b7c3ab2d68c..d51d5098231 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7a9d956826399b4a700a4f5d05eed66412f76f59a21a0995a85c69b3c528803a
-size 13124
+oid sha256:b0ae9f53b675f2f754f7cb3ebf3fbe45ae7eae0c63bc8628425e0bf21ff95bcb
+size 12485
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_5_en.png
new file mode 100644
index 00000000000..b7c3ab2d68c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Day_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7a9d956826399b4a700a4f5d05eed66412f76f59a21a0995a85c69b3c528803a
+size 13124
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_3_en.png
index 316ef83354e..5a1526f50b5 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8debba5d7b2f5866dafbb553732d5f99e3ecade1e3da873abdf2a82856f6b835
-size 12249
+oid sha256:da1945462f70133c47e33eaaa9dfc3d64033c6a83d4d50b03776adcc7a7f77e0
+size 13334
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_4_en.png
index 0f1ab6cc74d..316ef83354e 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fe20c0a4b18c1844df6d962e147a5e939cf0cba70657a67f3286c013942dd010
-size 13068
+oid sha256:8debba5d7b2f5866dafbb553732d5f99e3ecade1e3da873abdf2a82856f6b835
+size 12249
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_5_en.png
new file mode 100644
index 00000000000..0f1ab6cc74d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_PageTitleWithIconFull_Night_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fe20c0a4b18c1844df6d962e147a5e939cf0cba70657a67f3286c013942dd010
+size 13068
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Day_0_en.png
index 2b971f938f1..ca94a884e3a 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:448e4fea5f9363a84c020548764328e477662aecc7f423237ea06e439563b507
-size 25511
+oid sha256:560c9159e78e9da940da58a4297aca1ac647218fab0e03c532016ac96a3a560d
+size 21524
diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Night_0_en.png
index af0fbca7123..6793e1a4051 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.icons_IconsOther_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c4efddd547921361891b175554e3ae789259f7059fc18b68bcca2a38401f387a
-size 24742
+oid sha256:b0bb37fb7f6dbce206288431c59e3cc1b31d1a5a25d73b2f2321d7d46a459da5
+size 20631
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en.png
new file mode 100644
index 00000000000..137a7d8afed
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8070f1089c8d151b74558046ca70d3c92525d80109dcc082ac05be5678b7b6e0
+size 31230
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en.png
new file mode 100644
index 00000000000..7a2ce52d926
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDeleteConfirmationBottomSheet_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2dc3ac99e6894376a01f3f9cdbe8efcfd43233ada646944634489620535d326e
+size 29603
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png
new file mode 100644
index 00000000000..be45c32ec9a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4cded4f64be360fbd6ba607f9303e17154da24220712cf2e8da2d495b50bda26
+size 38103
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png
new file mode 100644
index 00000000000..440c3309cf2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de4cec2f60dda00375c6583fb2926cc0fdfa02d4673bd3d99bbe0ca3a2193952
+size 36454
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png
new file mode 100644
index 00000000000..02a746e1d8b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8b2613f0382fcc71f248e92a2ab007c20b88178f10f20e91a98f9112ac5dd3b2
+size 8226
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png
new file mode 100644
index 00000000000..9ccb8d78d2c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dfc1ae0e3cb25ee9e6092c2eda4b5e8d03ec3fae37e8d4d4d591e3d697042201
+size 12845
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png
new file mode 100644
index 00000000000..aea3483115c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:51c50604b17ac864c548a33141aba6bf9c1c791790a11fe692493a9fd3d91fda
+size 35598
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png
new file mode 100644
index 00000000000..d79c6a8d25c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:37f2c5ddc5dbf12e9006865a8e0348949e8811e9e3031721d67dcc0b878d00ba
+size 8011
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png
new file mode 100644
index 00000000000..04df2c1c11b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9e21c6ef5aeb835cee91a073ccd23a9fae1f3a1e6bc5c5dfdbe80ce9da62c51
+size 12496
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png
new file mode 100644
index 00000000000..11c187832fe
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:04c3a63bd1d6b1c217946fd3ac359ebbedd33d39da663b489397a4a81744c75d
+size 34365
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_0_en.png
new file mode 100644
index 00000000000..fd7b7e64439
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:467d501edd69dc1cc2ee57b2558ed6215f0464495d09a421692c2e30d4dc0fb3
+size 6473
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_1_en.png
new file mode 100644
index 00000000000..f835ed50a38
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b4a741963a984c6f491290fda0f399a73a92099cbef9a6c79676a1f89bbc53b1
+size 9050
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_0_en.png
new file mode 100644
index 00000000000..a0e5cbaec3b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:45592dde62aa3f5272bb63df450b5eb76f634caadbabc0ac416c27882edb2ecc
+size 6340
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_1_en.png
new file mode 100644
index 00000000000..8430ef926c4
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_DateItemView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c5298e5212daba4deda2bb931f9de1af660af559dc77f092622ded925125233e
+size 8898
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png
new file mode 100644
index 00000000000..56d561434cc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9fe856f45857f420a42562f0618473857eccd3f9a16c75df43157b881d473fd4
+size 8892
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png
new file mode 100644
index 00000000000..7c0ae7e7de2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fc84a1980024331be4906d1df8702ba16cfee553d18c3556d6c08597cc5c1a05
+size 13243
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png
new file mode 100644
index 00000000000..60abaf54cf9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:538d13a4aba7f8d7e503852eed6ae2fe35c1a54002c33abe7ef763052d4c7ab8
+size 36254
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png
new file mode 100644
index 00000000000..412d151f90f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:674d480011c737b98adc471bca330182efc6eb31b1671e6bdd6aa54bfeaada9d
+size 8616
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png
new file mode 100644
index 00000000000..89ea4bcb8b9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3ffaef3911f91499e4905e0d041c8ff31a6343c2b71ea79f0b4ab6d6c89800d5
+size 12795
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png
new file mode 100644
index 00000000000..31cbcbc59b9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b2f88af6da9f62f2fe0fb17883387574eaeca5d1f65f651d970e13c22ca8079b
+size 34947
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png
new file mode 100644
index 00000000000..a027c893039
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15d3fa6a95cda6bca06ad79d3f4862db05e38111cdcac47c1cdd3aa204bc1f97
+size 4210
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png
new file mode 100644
index 00000000000..503f2bb229e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:abaae9e0c6bf9d7dec701e9a51592e89408668e0a2b8325731efdfdc73978acd
+size 3667
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png
new file mode 100644
index 00000000000..f5cb7d82cdf
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:00159ed8d968d53970e4a3b7f82ab542fb5eabfc4513dd68d0eef05f0615373e
+size 7290
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png
new file mode 100644
index 00000000000..6665c107c11
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:efb12b63fde67256b255503d00848d64480e142c54619329b75aeed451a3dd17
+size 6375
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png
new file mode 100644
index 00000000000..d58f4c31555
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0f6835bd79d18d202c1d21b00a1afa4fa2c7cbfaf8f586a1dd1f48afdd5f69e5
+size 7644
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png
new file mode 100644
index 00000000000..dc363be75bc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:452afc2e04191eb82de772597ee97987eda5667ff56ecb684bb3b9e0bef90435
+size 6737
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png
new file mode 100644
index 00000000000..1c0a01c77f7
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4444ea352a367bb8617e9be4c86368b0a2292916a20a0c166933b219617e6055
+size 8502
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png
new file mode 100644
index 00000000000..f0839c51048
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eb103b365f83667834ccf8e6a181ab59d1c9dcbecf6fd4eb7f16bb236444b4e0
+size 9087
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png
new file mode 100644
index 00000000000..859c045a16d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0ce431cefbd22d8232af06626336e4c3baccbf9ad32e88f662efab66c7640b0d
+size 8737
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png
new file mode 100644
index 00000000000..2e0831e58a3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2a9292c525e7fe4d72362f2fe04a9f1184c7dff951f3b517e55f6f369214324
+size 8992
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png
new file mode 100644
index 00000000000..4387dbac9dd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d10b446f168e8a1295c811c57a08897db6636935646cd6c314eb7b6b7e310d5c
+size 9184
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png
new file mode 100644
index 00000000000..c69aff8a3fe
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:44b37f78992762569431bed9330140037b5fe9e49bb0043e2bb94cdfc2b53844
+size 7989
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png
new file mode 100644
index 00000000000..2acff7a04ef
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:54f3371f7003d7fe40dea4968037fbd4e148449e431356e693c7951a2814dab6
+size 8592
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png
new file mode 100644
index 00000000000..01146031c31
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bbe8efde3b6f6f13558eb323b7774640a269c66a5fe4b0c44e5b60fc6bccf4c6
+size 8329
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png
new file mode 100644
index 00000000000..38b1cfb67e9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e3935b49178db454d1cd20270a1641d380f5f3e576548d74e194fe7619bff4c8
+size 8492
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png
new file mode 100644
index 00000000000..a1e77b2b01f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0796d9962b93631f27338f9e93b9f7812964cd830cc1d62903829fd8012865cf
+size 8653
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png
new file mode 100644
index 00000000000..7234dad6342
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b98131ca183aaa3fd550f2b78317d73fb63ad03122c298d56d451d4023fd61d3
+size 8548
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png
new file mode 100644
index 00000000000..6e460fc429e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e0fb23e74fded45ea3d56cd69c2675efa2c0c0190b17bc8b4cc3ffbb2715650e
+size 10772
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png
new file mode 100644
index 00000000000..d9e5fffc64e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a52ddb20b4f07ecdfc23f5d19a26832d8d61cbc25159ac2868e9ff56d844e755
+size 36271
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png
new file mode 100644
index 00000000000..872a6e3520f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7a52b93594d74e66ac4c5145254feb4a5b82941783defd0e8820c6acc21cac97
+size 6900
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png
new file mode 100644
index 00000000000..5d2152939f8
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3d11986925680f2b7a9827858396e85ad13792b84412d01f5589a32f1a48c631
+size 8038
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png
new file mode 100644
index 00000000000..310557b2bd0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d440f08f846bec01f07d10ab2347dd5e863ae594109038dc9e6e4e40fe9528d0
+size 10239
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png
new file mode 100644
index 00000000000..ea2f0891afd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee9ef033060816da98e228abfeea3d09c2afc8822b884f1d656646dc3d15ce02
+size 34685
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png
new file mode 100644
index 00000000000..677aaf260ba
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1957dbe8c580b048fc32877639d42e3761b10a298dc80804634481ef391dcf76
+size 6544
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en.png
new file mode 100644
index 00000000000..b17c686b188
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8b04072b9e16342e333c2a10e6462420216b4192226c97f0d4bd5b5ada1aecef
+size 18791
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en.png
new file mode 100644
index 00000000000..9cec9619662
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_10_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d90abb10774208a842a3284346c3fa1d3c8e34b53daf6ea0f14f61bf4be5bb87
+size 14551
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en.png
new file mode 100644
index 00000000000..f7d527bf6db
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1853de49049bcd1448cc4e7c4a38ed8ab3cf11c3d11b1ddf296f2b6a37e985b4
+size 15428
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en.png
new file mode 100644
index 00000000000..0d080b8cd01
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e74c3ab733e969059c4d8bb72903ab53aeb855f49857656bfe61f4dee574dbe4
+size 70317
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png
new file mode 100644
index 00000000000..9807f8bd54e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ee8d9e829efc0723d62411e2069a8dac90b18069d1d8cd5dc5ec6a5b9899a14
+size 21572
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en.png
new file mode 100644
index 00000000000..1f1b966fffc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7d1cdeba30c6efca096c3625613ce5ad33b6f09e08eb09b2758d9a039e4206fb
+size 15106
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en.png
new file mode 100644
index 00000000000..1f1b966fffc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7d1cdeba30c6efca096c3625613ce5ad33b6f09e08eb09b2758d9a039e4206fb
+size 15106
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en.png
new file mode 100644
index 00000000000..07edef2141f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cbcf086763463eaa1dbf9cb52620c430f7a7982f01d3abcd039ebd307544f8e7
+size 72986
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png
new file mode 100644
index 00000000000..ca39d4cceaa
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ff96b22724d7f82b3003a73a560da0a34c9c196757b4336706b5823bbfa32589
+size 32398
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png
new file mode 100644
index 00000000000..cc888a4d576
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:492b5ae698dc52672d8d0a4599c9cd9a5b6f414e8a0a6f42c91e765e5a5b221d
+size 40921
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en.png
new file mode 100644
index 00000000000..5d694d039be
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_9_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:782fa9e5501e399d4840c0aab6ee317aa4fa8137eab93ee85924ec512b071be1
+size 14525
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en.png
new file mode 100644
index 00000000000..8baa855f7bd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9d7fecddc7aff63c795dcad68665dc1771544f5facda5a838b1f3391655ee49
+size 18306
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en.png
new file mode 100644
index 00000000000..6ead8ed8665
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_10_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:49e6a6bda914fc5e77bd0a864900f4fd7f654f4017a331be6008825b2150340d
+size 14013
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en.png
new file mode 100644
index 00000000000..fa9f362ab45
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:739e2618bac9233b0ff7335d734d7fb594e3ee8860f9e61ef80d2dc4d7736a27
+size 15026
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en.png
new file mode 100644
index 00000000000..8844a9f4a36
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9833ad112df471f8be9587999f444ad371ae1eebadfc351cea83c6db5685c9ad
+size 62028
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png
new file mode 100644
index 00000000000..5c067f246f2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fcb5bc041286ba863ae982b2ad03873a76e48ed6ebd5d35c82dea269d86363a7
+size 21189
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en.png
new file mode 100644
index 00000000000..73e39f4bd62
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8be4c40bcec9cb84bd087dc395be694b614f88ff5b44a33dafc3ad87576d23c6
+size 14578
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en.png
new file mode 100644
index 00000000000..73e39f4bd62
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8be4c40bcec9cb84bd087dc395be694b614f88ff5b44a33dafc3ad87576d23c6
+size 14578
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en.png
new file mode 100644
index 00000000000..700079a0db3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_6_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:267cc528f2fdaba66bfad4f8c8622087b76c2e3409f5fda8ce25009039278a22
+size 64356
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png
new file mode 100644
index 00000000000..2149a46ba7c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f5831741183467f1d05517097f2617aee405a9d6752cdf8a8e193e5851376a3
+size 30904
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png
new file mode 100644
index 00000000000..158c1812401
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de98370531bc9342539bbf98b6f3534b72e327a94e34b1c6d827e2330291340c
+size 39235
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en.png
new file mode 100644
index 00000000000..4ae1e1c6cf4
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_9_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c08080c2814f8e8273949b39359ad105f0305ee6c7b91ddf9b437ce925489b40
+size 14125
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en.png
new file mode 100644
index 00000000000..70d447adcdb
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7c7d4201ed9aa37995f4ab8ac982404f59e77374f316a057685886f14e698c35
+size 24680
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en.png
new file mode 100644
index 00000000000..41b0cc2f9a7
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b94fd31b7ed71eacfe8f136bfd59405b85d31a0fe557800311794f4ba7006271
+size 22749
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en.png
new file mode 100644
index 00000000000..876fab066dc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6520b0faf9c22ee1d9b1a088b29fc07e3d8004db5a342cd3bff189d844aace6e
+size 24649
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en.png
new file mode 100644
index 00000000000..50727c043f0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.audio_MediaAudioView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5dc15cba85c8cc376b780df648b4fa265c054de07a4bab426569ac26be03fec1
+size 22784
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png
deleted file mode 100644
index c4d024965ef..00000000000
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Day_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:5307e90428957819812269b9b3e0c6e9d59238141d54cd959aa5506290797a35
-size 11587
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png
deleted file mode 100644
index 37f1c2ed228..00000000000
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.file_MediaFileView_Night_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:52354bcf471b14e38e582cc29f73407c8ca65026b2b7c6db3d3b28ec94950679
-size 11413
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Day_0_en.png
rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_0_en.png
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Day_1_en.png
rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_1_en.png
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en.png
new file mode 100644
index 00000000000..b5b75d1b639
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a6d04e0ee068682ebb0a3842ba73407855f2b83b7389d26fa0f3e2ec20d42dc8
+size 7389
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Night_0_en.png
rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_0_en.png
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en.png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaPlayerControllerView_Night_1_en.png
rename to tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_1_en.png
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en.png
new file mode 100644
index 00000000000..336b57074a4
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.player_MediaPlayerControllerView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1211fac03e492532b1e90608fd931b8fcc15dac695ef5610069bf512c2a5fef1
+size 7548
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png
index ae44f16e97f..fd5c2af6e60 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9df1a0ff2b3aaab2d1a346cb134b560d65c462286575ffc6124099529562eac1
-size 389594
+oid sha256:66437179fb0b851d4d4d647d00cab94cc7422d625f559839c675b378dbf1af38
+size 389408
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png
index 347f2dd6c29..6a81f3f1294 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4fde3cef34d23c894ae480a1b4c961065f244d845584f8607c4bb21e0a7e5f10
-size 388615
+oid sha256:2b286342ff4d46637beac1f980294f77b3e2eb6824d56448cdbdce7b41c911ab
+size 388612
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png
new file mode 100644
index 00000000000..4a43ce31dae
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8165bcb4b0d52a227aad4e1f3951fc3628ef647947ef2584ed50d8ede8a6a344
+size 38248
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_12_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_12_en.png
new file mode 100644
index 00000000000..f3d0c19a9fc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_12_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c9efb4ed1ee82bba30351bf213f0873637e8194140a3bbea669321bf76bc6483
+size 31449
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png
new file mode 100644
index 00000000000..6d8afe11407
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5d2882d79b9f66726c6d16c8c6fc84cb9f65a5674222fe85794229dc5ba12a6b
+size 24679
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png
index a9ae9d7999e..12d5df3fa13 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cf07ed00618c0552205b1e726050382369041bb7f2d9742b754a5652eca48265
-size 389631
+oid sha256:da172fdf40dc8702bc6dcb89bcc75e93bd279f6bbb9454f5283febe4da25d399
+size 389440
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png
index 173f440a849..e3b32d36d37 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:143285221dd5a1e714a9897293f13c45ca567635e60350738033b315f44f742a
-size 94735
+oid sha256:bb79e754f9b4caeb40508bdc067d68a4e115e8a50467fc006be6f5db0684ea5b
+size 94672
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png
index a63c5a2230d..40ff36cd942 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a4e1b6a7a3dcc1627aec1767ac8edac95bec483ebdc98d71d65342084e08a4dc
-size 396792
+oid sha256:7ad6e45382dec9bb27593b6e2ed92ed633204479040ccebaf4d362bb2f41fec7
+size 396403
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png
index 585ab58d5e4..d94989c7571 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6c3c2b7b1d64387ec1109ab21304df50de5bd4fac0317c27a2799e7405da3843
-size 22219
+oid sha256:d10cb9be5b5139f0fdfdfb11cc3d3eca1955297180e5db8142bfea6250f20d73
+size 25811
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png
index 94ba7bede2a..3603786361f 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:364fce9e921a21dcaae9ef1d7c98efe47c4c9118d23958550ee9eafaf202e2fa
-size 5751
+oid sha256:69b5d7572ae6e4ff084867fac1bae41a55c75c9a5236cb6ccb4c31b89ef77898
+size 5442
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png
index 72bdd87b183..6b6e6a655da 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:769a00d9e84cb2da43d790158f35e66bc5ed6538fdcb7f5e316f5de13d9bd9ad
-size 14768
+oid sha256:f8d3e3f8733424870b254be90599ed1ff6ba784089600bcb200fbef62c81537c
+size 14562
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png
index 7658ec53754..32e7fcef893 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bf1804b21f3d383c94e28714746ac61987643c8ec0480d2850b19eb01991aa4e
-size 15043
+oid sha256:d1785d90957316791969f047e66bd779da62d675004914099f2af2b69bebe405
+size 14700
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png
index f9276290071..dc33c0aef45 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ec58fa08f3c160e1b7d462b580f79b1c05a5644b9f807a79cfeebbeea417ed10
-size 13502
+oid sha256:1ddcd8e9e20de4171a3d9f8175806a268723e47a14dca431849c2c29edaf5d0b
+size 26267
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png
index 4394560d46d..d115aaa7edf 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:689081427ece8bc009266a50b9d2a80def581f51f00bf65b35d295b0ea7dcab3
-size 13736
+oid sha256:035ef0079af6e9825a52b86e2eab50667404a66d70dd2756596b60cc1cea376a
+size 26404
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png
index dfb1958e282..92093d1e759 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:177e16989220804b9472ec9e7435e1a6cdb14dc3a26c17b5b00cece634439ff1
-size 12602
+oid sha256:78251d1ca6ebd636376dc200e06d3b033b5d9e9df3e32e452f9882663d06ffec
+size 8676
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png
index 257bf3cd99c..afebd5ed8f0 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:58b45b8b1a73eec401d922e8f422902e3a594927a26100345807fa654d83929f
-size 12433
+oid sha256:c8e2a46f08630a184d0b6e182e31712aa959f2976b348b7d4b4a8f50f22bf2ae
+size 8473
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png
index 2bb281cfd13..f6f752ccf95 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:71550eeba48a6c9ccd4c26cb0e26f0136e8a5611089a8ea6c7cc92a8133b82d2
-size 60259
+oid sha256:6e78e412582b829f3504227b4f3b8d0180ea1f9f71ded30213e0c1153ba4e20c
+size 52481
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png
index 6fd5454a37f..049fd8c1ffe 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4f1f000c6baf5be3468b2a28ea43f2131d20a73cdbf1c22f52ce075b3f36bf25
-size 58223
+oid sha256:ff436720455e8951c96578613fab9980de42459334fa1d578c20a4c633c9f6a5
+size 50909
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png
index 5f3d66edeae..d2b38ae3dfe 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8c4220a5d9401d42a905dcf204bd273e8039d77a889209d4eb35de7c384ed05c
-size 61113
+oid sha256:3f5eef2eb386333da113037b088fdd0a565b0816bee70f2f7decca88893a3164
+size 53457
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png
index 80e866f801a..3ebb4b2f18a 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:009a1fc06c9a2e2bc112c4483002b43e9710c5ced3b2668c40c91b7c54d43b30
-size 59382
+oid sha256:5d1aeebf8addea8474ae7e97fa1fa064d7b5f416aab2ba385135b12ddd44b5d5
+size 51767
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Day_0_en.png
index d2843698935..c575f5f5bc8 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2eb0eaa26e48d182bd3dc78671cb43f02c24ef5f15fb4e6e8b47da2f3d37ce7b
-size 56567
+oid sha256:1aca6febd470707f5fd0c6919f4ad55cf7b0b7b9afe5c34db63f0acfa059120d
+size 47506
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Night_0_en.png
index 101c090e864..af28c0055ff 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerCaption_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fcfa2769e764b164375e6f6ef5194ce9ef686b6f76e16848270380a88beb175a
-size 55518
+oid sha256:c908b06d3d00253a8d2fd0231bbdd6bb5b0a7574c4599814356ab3bef42192cb
+size 45974
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png
index 17daeade89c..a3a8fad8b23 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4f6f448d800b741e7e59a44b0f660f4702603e394df6223b1d968c862266083e
-size 59832
+oid sha256:149e24482adb4b4ddf1e54fe39ab56315e73598f0dfec4acff479c89509070f9
+size 52144
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png
index 270f2ab1d10..cd34c866fbd 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:81bfbaf9030bc40ca6744dd6bf8170243243ebcd98fbc3de71fed85798d60412
-size 58150
+oid sha256:e9fb48855ec77da81d4cba8f428fbfa69039020a81b6ccea764413e42210abb2
+size 50860
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png
index 2bb281cfd13..f6f752ccf95 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:71550eeba48a6c9ccd4c26cb0e26f0136e8a5611089a8ea6c7cc92a8133b82d2
-size 60259
+oid sha256:6e78e412582b829f3504227b4f3b8d0180ea1f9f71ded30213e0c1153ba4e20c
+size 52481
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png
index 6fd5454a37f..049fd8c1ffe 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4f1f000c6baf5be3468b2a28ea43f2131d20a73cdbf1c22f52ce075b3f36bf25
-size 58223
+oid sha256:ff436720455e8951c96578613fab9980de42459334fa1d578c20a4c633c9f6a5
+size 50909
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Day_0_en.png
index 50ff6999afc..f2a7b25e4fc 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4dd98ff64e3ffd15c4004370ea627690e3bd2e186363cd584fe632f37c50c0eb
-size 60036
+oid sha256:fc2b0061acf3b93e9c27f14f27c5422b034f1d82a579e31b34ce6c09cc255751
+size 53468
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Night_0_en.png
index c04d42a89d2..b8ed083d9f8 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerFormatting_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1c6f606b522f53ed525cee3ecf798b7fb15cd907fd2caf8a98625b0fc3a5e720
-size 57708
+oid sha256:a5438d8a7c75ebb8b90bc7cb0086c64399546163393752ec0ef96796903cb391
+size 50962
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png
index 28c77cdeacd..26cdb69f7b1 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ed6b061bc090ded2fffc3af1003e0eb8fb0b933d7bb826d6bc31e6e633f11f26
-size 82464
+oid sha256:a315b0a33c2034828d8943f64ad88f3a2e3b4a39db17e4885a80d387be0e1a06
+size 76291
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png
index 6566c02a270..f3152152bd7 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4ef12ab84784c2b8dc91f1521c4a6191ae7fba10fa9da4fe74805078f7aa8bdb
-size 65823
+oid sha256:49c600c54b3ba8eb98af152a9867f7a1d8740c76f51f4b0d85af95fb5f716e34
+size 59323
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png
index 23c35528a9a..6afb4ba7c2a 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6508ce0bdd1f2e56f6ba63a69908bde3e83b169c6681f378850c16b68dbfc21a
-size 80509
+oid sha256:0fac85629625d4e47c266e54105dde49b28036726718afde8e5dff7cb4505b3c
+size 74264
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png
index 2f5e8636864..948e5367228 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bbe4d0343d8cbdcdd4c2026cff3e6e015fe0b1b9cd34cb1a54e4fa1fcee563e4
-size 91834
+oid sha256:1783e2299851c1a82a57b5434cc5488c94967622f886c267fe5346c42f9c52cf
+size 85709
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png
index 516ea20b85f..288474acc6d 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2bb2e3854f3c9d5ddd2f40ba7ef0f8a6e36da232f029044c36491e59bfb95ead
-size 69082
+oid sha256:a8b5f3892fa9bddde1217f328ba39b9aa99080c2dfd68e50221361e9529c0dfa
+size 62559
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png
index 15e1996bf6d..733a723c6a3 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:83ae14f812b9b1ade316b017ca7334a1550b9993c1b284018485fb9e3c5df3a1
-size 67376
+oid sha256:58eaadc8d90b16008dd83040c4bf6d3bfc9fc8eb6bcb272bedf47317977dd14b
+size 60814
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png
index 2ce7eb8a4d2..6bd49bfd243 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1b107f28fdc4020cbc0a21a6e78cca7effb5a166080803315b97dd5f2b8c4a7a
-size 75497
+oid sha256:509f8ee34c3d526f3c96578dbe00c9dd63e2178bea8ea7edc8467cd12cf8f622
+size 69346
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png
index 9799a6fc5cd..dcf27b2bb2b 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:915fee1b03b18db0f372d3e31e81cc0606dbe28e9a33aeaeef68d3476c5c8509
-size 66276
+oid sha256:c666853bb32e576090f3d314097dbe7d2daa85bad949ce9c3ad152847c743a0e
+size 59726
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png
index 9494d1ff327..3732d0b8f22 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8bee25ec96fd67cf9ade2e54d44ec393ca9ee4070a725eddec64f134207c7a2a
-size 67115
+oid sha256:19b169ca0a32a4aec13012495fc2d83ff53ab2d096e9d7630d2e84317302c67a
+size 60548
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png
index 9888ab99ce0..48b8b9f4610 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0b941b5215d059929b5ef64de769bfe3eb594a984237c110d8203bf763fa9322
-size 69286
+oid sha256:590a064309be74697bd0ec036c2112be286d16fbb944246f9091b3ffce93dc8e
+size 62766
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png
index 9d6a6c4e118..b9461d92c88 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:90baf1795797e36aa26ddd2a0efa40f0ee893cc985606c55d184f60a57cda93c
-size 76020
+oid sha256:e36d27009e23535f39ea20206af67a95368d0cc674d53e9085b111e48a6beea9
+size 69949
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png
index 1ba8016b16c..ccc32f4a1e3 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8230e078a5c0aec1791b1cc1ffd813660a1bd64d0a082ee9d5cd37024d923d81
-size 66567
+oid sha256:405f65309e783c2c099f45e6be528c05fbfddebd37accd9352a9208588b6e178
+size 60015
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png
index cd1fa5cb75b..2685cf557a8 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b023ac31c1df8e542e4ba012c14aa0f083000cd82687628b78d03c7fa0fc7462
-size 79967
+oid sha256:c6f039f8db0f5e95ad240697157dddd2115876deec42616a6b43ddcab6e4d9d8
+size 73593
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png
index da99e6a4fa1..80fa524c9a6 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_10_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0011599e9bcfb6cfce356e4ded07053927b1c2971db457a1e68235b4fd805bad
-size 63814
+oid sha256:15a0cff9d79d66b032a5b8cf99e731299fe56bf8de7328c858745974bf776b8d
+size 57060
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png
index b4f653fcf83..ac0672cc0bd 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_11_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b0ad6ff8d00e0b8880428bfc4b17fa1b2accb0117cd48a3c6630e6d8577c0b38
-size 78103
+oid sha256:6ce61ea0b2c06cdd7b1ea0cf3dade22cc56c8434908d540b35752b3af6a6eaed
+size 71709
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png
index bc98034f4e3..b29d8c5a40d 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e42199bbe520151ba537e93e87911f0870437686128684227044c48bcc3fb754
-size 89090
+oid sha256:5db1a345670e7ae8af6149e97bf1dc6362326f1de55e4884a95b248591562f3a
+size 82831
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png
index d568fef9875..c9992326af7 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7212eef12bdf4e6bcf8db3a1471b279d21bab71bc68661a0b09cacb6d062d35c
-size 67125
+oid sha256:f455cd10db8660d33aa51b6c487d2dd2b86674d94a60a38a83cff3785ec48524
+size 60328
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png
index b3fb3dac634..40c7888405b 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_3_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:381724bffe2b22e3c768addc1016d8d77d1e2969c43f76c7b4aaa59e8d0c2700
-size 65452
+oid sha256:f7b23306044dec23152302ab6ffa9f32d5d4eee6e95cdfb28453e04007e4906d
+size 58692
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png
index bb16d1803dd..8da7483264d 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_4_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:603baddbff53df34bbe7b3a1ce4119f20b8b3255da86e80b6255a0ebc55661d6
-size 73862
+oid sha256:b3013792b094d1224e76b9428818083f4d586e759f3de412e6f1139df572b9d0
+size 67069
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png
index 67dff832229..e6a5a6b5645 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7bda76b1f85a8c9902f634ca2f62bbfc33657cb77643cdb80ae25f1065fc9802
-size 64412
+oid sha256:d4744282f54264abc7cb833efb7b178acd6e334bf03905e0a0f5d1c21b427da9
+size 57653
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png
index 61d867d5f21..682e6c8ecfa 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_6_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:86de5beaddd424dd827f2286b1b76374078726776ff3faf33ecc73693a37a9ab
-size 65071
+oid sha256:c94c6a421aba335a6af4e6548d0ceba7f09e1dfe34f8813072ddff943eea8aab
+size 58312
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png
index 602ea537352..242e5b0c0f8 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_7_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5e4e6decdec0cbf92774c9f2ae8cbca23063637de9dff06eb1f19488bc66c3b6
-size 67391
+oid sha256:76de82231ffb2d5f39bd5e393159495b1c0f84710c84d69fe678a7831cc383bd
+size 60615
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png
index fa0b4f95d33..7fba8057e4a 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_8_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:18eb91d33345655ed1fad1ff08798e37aa812e7dd11d2352ac35897f14246f2a
-size 74340
+oid sha256:ea57d3607d3a9f1db5a0f7a111de31859c67c17da09cf0b0fb26485c59e41eff
+size 67533
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png
index 7d2ce124199..fc9cc280cd9 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_9_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e08ac324ef5285da4214adaa4b692e74a78cdaa12a4b23127ab959e34b7d83b3
-size 64614
+oid sha256:14082dae20c3a415221fabda507a6d0b338c04f114319fd53bbcf4b1ec4da549
+size 57868
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png
index e65c6cc81f3..05cb8123c29 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:05bb6bfd1b1e35015dcbebd9b96c0729e16db6e9a37741e35d1baf4e7c3f9009
-size 51684
+oid sha256:663a2f1bada754dd92043b26fdafdcb4974f1dfe8368e4126bf41baa4356e0ac
+size 46655
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png
index f715d5a1157..6bf091cc3e4 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerSimple_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5a86352ebab4c72f028e476db8fb8b514f57b6f237788583533088d05b8fe1be
-size 50646
+oid sha256:cca813483d17bafd6b2c09759659525b59909d76b90e124e03e7f8e0a161121e
+size 44476
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png
index effc0f4d8bb..be6cdff9359 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a19ef7fac4398287f12076331f774f7b368980787af5f9d205d1e75c0f382525
-size 30324
+oid sha256:884c4844e2b54a83fbc78cb81262fac15d99d3c45cc51888ec8666bb0bd8bf38
+size 26260
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png
index 2c75b11c3d3..ba7be13749f 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:231e7a071cb71f78a61eeacff63f8726bac02bc0c0011d1125fd0b3916f2b078
-size 29288
+oid sha256:64411c282cfe9c9100df5dc4b503a9c7544252286ccf17ec9c64af73e1b0616b
+size 25275
diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml
index 1e73ef425d7..0ebe19bcc43 100644
--- a/tools/detekt/detekt.yml
+++ b/tools/detekt/detekt.yml
@@ -224,6 +224,7 @@ Compose:
- LocalCompoundColors
- LocalSnackbarDispatcher
- LocalCameraPositionState
+ - LocalMediaItemPresenterFactories
- LocalTimelineItemPresenterFactories
- LocalRoomMemberProfilesCache
- LocalMentionSpanTheme
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index f18744bae9c..2efd8eac976 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -80,6 +80,12 @@
".*voice_message_tooltip"
]
},
+ {
+ "name" : ":libraries:dateformatter:impl",
+ "includeRegex" : [
+ "common\\.date\\..*"
+ ]
+ },
{
"name" : ":libraries:permissions:api",
"includeRegex" : [
@@ -92,6 +98,13 @@
"error_no_compatible_app_found"
]
},
+ {
+ "name" : ":libraries:mediaviewer:impl",
+ "includeRegex" : [
+ "screen\\.media_details\\..*",
+ "screen_media_browser_.*"
+ ]
+ },
{
"name" : ":libraries:eventformatter:impl",
"includeRegex" : [
@@ -165,6 +178,7 @@
"name" : ":features:roomdetails:impl",
"includeRegex" : [
"screen_room_details_.*",
+ "screen\\.room_details\\..*",
"screen_room_member_list_.*",
"screen_room_notification_settings_.*",
"screen_notification_settings_edit_failed_updating_default_mode",
@@ -286,6 +300,14 @@
"screen_join_room_.*",
"screen\\.join_room\\..*"
]
+ },
+ {
+ "name" : ":features:knockrequests:impl",
+ "includeRegex" : [
+ "screen\\.knock_requests_list\\..*",
+ "screen\\.room\\.single_knock_request.*",
+ "screen\\.room\\.multiple_knock_requests.*"
+ ]
}
]
}
diff --git a/tools/sdk/build_rust_sdk.sh b/tools/sdk/build_rust_sdk.sh
index f0c03783339..7c65604167a 100755
--- a/tools/sdk/build_rust_sdk.sh
+++ b/tools/sdk/build_rust_sdk.sh
@@ -59,6 +59,13 @@ buildApp=${buildApp:-no}
cd "${elementPwd}"
+default_arch="$(uname -m)-linux-android"
+# On ARM MacOS, `uname -m` returns arm64, but the toolchain is called aarch64
+default_arch="${default_arch/arm64/aarch64}"
+
+read -p "Enter the architecture you want to build for (default '$default_arch'): " target_arch
+target_arch="${target_arch:-${default_arch}}"
+
# If folder ../matrix-rust-components-kotlin does not exist, clone the repo
if [ ! -d "../matrix-rust-components-kotlin" ]; then
printf "\nFolder ../matrix-rust-components-kotlin does not exist. Cloning the repository into ../matrix-rust-components-kotlin.\n\n"
@@ -71,8 +78,8 @@ git reset --hard
git checkout main
git pull
-printf "\nBuilding the SDK for aarch64-linux-android...\n\n"
-./scripts/build.sh -p "${rustSdkPath}" -m sdk -t aarch64-linux-android -o "${elementPwd}/libraries/rustsdk"
+printf "\nBuilding the SDK for ${target_arch}...\n\n"
+./scripts/build.sh -p "${rustSdkPath}" -m sdk -t "${target_arch}" -o "${elementPwd}/libraries/rustsdk"
cd "${elementPwd}"
mv ./libraries/rustsdk/sdk-android-debug.aar ./libraries/rustsdk/matrix-rust-sdk.aar