diff --git a/.github/workflows/generate_github_pages.yml b/.github/workflows/generate_github_pages.yml
index e39175c755..e63cb52cc8 100644
--- a/.github/workflows/generate_github_pages.yml
+++ b/.github/workflows/generate_github_pages.yml
@@ -12,7 +12,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == '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
with:
diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml
index e49bcb9663..55293f972f 100644
--- a/.github/workflows/nightlyReports.yml
+++ b/.github/workflows/nightlyReports.yml
@@ -18,7 +18,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 161e2ade89..17012282e3 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 a3fedc9a6d..ef7a8843c2 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 1a70b1661e..dd6cc5e64f 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 c224ad564b..bb4493707f 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 ae6f5772c6..7cbf455ba2 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 3dc07f4c66..53d6eb2e12 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 5ae9b0c2b4..b486d55ecb 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 940e576a30..93fc8bd06e 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 31edb5b08c..942fdd3788 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 9d5bdafb7a..738bd12430 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/40007060.txt b/fastlane/metadata/android/en-US/changelogs/40007060.txt
new file mode 100644
index 0000000000..1bc0b2f8e6
--- /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 ee935c8c60..4a905afaa5 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 205f1db36a..c60e10660b 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 3b8c5c0812..875142bc01 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 5e89edb1fa..c420fcfe78 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 55af91d9d7..16202e2f87 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 0000000000..c3eca7567c
--- /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 0000000000..86483aee70
--- /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 0000000000..0215b5cde9
--- /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 0000000000..2664528d74
--- /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 0000000000..9fce2f2173
--- /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 0000000000..14239d93ef
--- /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 0000000000..f155cdb4a3
--- /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 0000000000..80d662bc5b
--- /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 0000000000..0324239fd9
--- /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 0000000000..d029b80906
--- /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 0000000000..cfecb8355e
--- /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 0000000000..658717d48b
--- /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 0000000000..5d45281d8a
--- /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 0000000000..f1df84beab
--- /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 0000000000..0880233ff6
--- /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 0000000000..1c1a17767f
--- /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 0000000000..fb08821387
--- /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 0000000000..c685f1cf37
--- /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 0000000000..23b1025ce2
--- /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 0000000000..ce8d602861
--- /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 0000000000..6ea13f16a1
--- /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 0000000000..fa33b074a5
--- /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 0000000000..a8d898b08e
--- /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 0000000000..09f916ae09
--- /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 0000000000..c6aa055cd1
--- /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 0000000000..70e43ba076
--- /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 0000000000..326227df48
--- /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 0000000000..79d9c56964
--- /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 0000000000..08b8509d31
--- /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 0000000000..85da9d9f50
--- /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 0000000000..2ae4a79857
--- /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 0000000000..ebdba8074a
--- /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 0000000000..1023af2d28
--- /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 0000000000..1504ef8631
--- /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 0000000000..454bb8e193
--- /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 0000000000..7027061804
--- /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 0000000000..ec594086fc
--- /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 0000000000..d74155ead1
--- /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 0000000000..af2bfefd16
--- /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 bed2bdd365..2c801ed76a 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 0000000000..b1262708fc
--- /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 8b01b00afe..d7ad980dd1 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 c60c49c81b..fd9b1ccf1c 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 dcbae607cb..74c4e424c0 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 740025ce17..f8ce40f1cd 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 26c4a1ce6f..46dfa33081 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 b9984b1dc3..fce1142f2c 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 6ed4ea81be..ae2abef6e8 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 b77df23254..8596647aac 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 2df43b3fae..3a65239697 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 cdb48f8f63..5f6fa68ff6 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 2b8071fd0b..5b82829b80 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 c21194989f..5e8f9d468d 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 824e8c1692..f5b6520996 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 8cbb7b6d74..1b9c9909f4 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 7d5bad4d63..4ee44fbc80 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 4895390ab8..5fc68a0c61 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
@@ -274,7 +274,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 47e4721f7f..c0af0088c1 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 ->
@@ -398,13 +402,13 @@ private fun MessagesViewComposerBottomSheetContents(
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
- }
- }),
+ .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(),
@@ -452,8 +456,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,
@@ -508,9 +512,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
) {
@@ -539,5 +543,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 411ff37c8f..d631cf3dcd 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 75c598df36..56bc1ca0bd 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 a5f027a535..2fef1fc525 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 7d30edd116..4cf0928d5c 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 f700dcc6b1..bd506fa3a5 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 8eef2d7619..a8a42b17ed 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 34c58bdb06..5dc55b0dc4 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 48fdb83d79..1e86d4af08 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 b91b4ccc17..9ad875377c 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 35a8cda293..3fa786f902 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 f5cee592e2..365b97f9fc 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 28a0ff094f..47b2b4eba5 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 d94ca9013a..3700e02ccf 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 41966c036b..cd680d4e80 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 0a392aac6a..53237ef4de 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 b312024ebb..d34f63ecbd 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 4c2a66334f..6fce42474c 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 c1acba9be8..229a02b714 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 8cb7464a9a..1369b5af63 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 1d4e1a43b3..c8305f971b 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 49db6f6c95..cc4120ffab 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 0000000000..9866d846ec
--- /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 51c4cb43ba..df76e15b6c 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 7043d3f848..59545d6674 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 a7cbaaffd2..f1389962b0 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 56edc949a1..bd5e9c0c54 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 60814477c9..1c667efffb 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 081b5e53b1..8a0c8ea5d5 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 6dfa8df752..d3e67e223e 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 844474e1e1..c4896ca5dd 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 e4c8641197..7c2b9438ae 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 601ed2ee7a..8742e4746d 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 0c758852db..c2c2fd0e21 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
@@ -270,7 +270,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 5acfeff05b..e47167e32b 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 6255c7c2c9..42ac543aee 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 5568b2beba..a31d1c032a 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 e21171120e..a2d1391280 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 231161e583..bfb2cfde7a 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 12cdbdcadf..f89953263c 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 19c0b4ffe4..3b3d11fbd9 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 586790b618..50c403fe87 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
@@ -35,9 +36,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
@@ -69,15 +72,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) {
@@ -90,6 +97,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)
@@ -101,7 +109,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
@@ -109,6 +116,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) {
@@ -149,10 +165,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 d43b0a813a..85b5340959 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
@@ -40,7 +40,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 49b9f73cb5..b3a4c0e7ee 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(
roomId = roomId,
@@ -124,7 +127,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 7e89c3ef07..5e65ce9336 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(
@@ -206,13 +208,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()
}
@@ -231,6 +243,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(
@@ -525,7 +551,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) {
@@ -555,6 +581,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)) {
@@ -610,8 +649,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/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
index 06748b0999..60aa00150b 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/res/values-be/translations.xml b/features/roomdetails/impl/src/main/res/values-be/translations.xml
index 7cbf1fcd6d..6ba7e59a2e 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 09003dc610..2946530857 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 f3bb984173..643ed52d98 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 57874ff30b..88d651a83a 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 a8c197ce7d..7ff262314f 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 ca25c83461..491435d52d 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 db8b78d779..7aa3a105e1 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 5766d1e0f1..2e28e6e110 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 8f992b1c7d..be27d0fb32 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 cd3ba93146..730adaa293 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 fe752f2874..eb48791320 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 a3a4dc965a..6c677ea68e 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 80195a14ea..aabbadf3fe 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 5dde97ec98..294c0e4a76 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 0ec4c6fd80..42e8fb0562 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 c905333c2c..4b2d289b35 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 eed1cfcbb1..7f20d978d4 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 616f293554..49a7f147cf 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 a2f6e26e9b..1198de917c 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 19400c85b3..bd602d3ed3 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/RoomDetailsViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsViewTest.kt
index e4eb8d5f69..11858929e3 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/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 6ba1d8ed8e..f44f192e77 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 9a8f2353ba..67c4544aaa 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 a2468c32d0..fdd92e0c44 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
@@ -146,6 +146,7 @@ class RoomListPresenter @Inject constructor(
AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData())
)
}
+ is RoomListEvents.ClearCacheOfRoom -> coroutineScope.clearCacheOfRoom(event.roomId)
}
}
@@ -255,7 +256,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
@@ -312,6 +314,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 a6b9673b54..a0a5633165 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 6b74c7934a..36b951f73c 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 534de3c4d4..a77f1545da 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 315efb3478..04fb371eeb 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 af305d4b7d..cd248d7372 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 69e9a7d401..f43c081ea3 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
@@ -31,9 +31,8 @@ import io.element.android.features.roomlist.impl.search.RoomListSearchState
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.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
@@ -188,6 +187,7 @@ class RoomListPresenterTest {
createRoomListRoomSummary(
numberOfUnreadMentions = 1,
numberOfUnreadMessages = 2,
+ timestamp = "0 TimeOrDate true",
)
)
cancelAndIgnoreRemainingEvents()
@@ -288,6 +288,7 @@ class RoomListPresenterTest {
isDm = false,
isFavorite = false,
markAsUnreadFeatureFlagEnabled = true,
+ eventCacheFeatureFlagEnabled = false,
hasNewContent = false,
)
)
@@ -305,6 +306,7 @@ class RoomListPresenterTest {
isDm = false,
isFavorite = true,
markAsUnreadFeatureFlagEnabled = true,
+ eventCacheFeatureFlagEnabled = false,
hasNewContent = false,
)
)
@@ -335,6 +337,7 @@ class RoomListPresenterTest {
isDm = false,
isFavorite = false,
markAsUnreadFeatureFlagEnabled = true,
+ eventCacheFeatureFlagEnabled = false,
hasNewContent = false,
)
)
@@ -633,9 +636,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(),
@@ -652,7 +653,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 f02c53e6f6..1839b35688 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 8a26120a9e..41996b24db 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 7e91fa59de..fbe7137ed8 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 6ede9544ec..0d86860445 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
@@ -12,7 +12,7 @@ import app.cash.molecule.moleculeFlow
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.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
@@ -143,7 +143,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 7d03eb7fba..0cb87cb762 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 8d0c57cb7e..7daf180c6b 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 867d43b7e7..3b3f2094fe 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 d6b629ea93..e9b8849a92 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 c78c7ad85a..eed0e7f576 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 ce0d4a07f0..d795f60715 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 e3ce329b09..da86349adb 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/res/values-it/translations.xml b/features/userprofile/shared/src/main/res/values-it/translations.xml
index daacd44255..277ed7e27e 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 ebd897d84c..601e7cce16 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 ec67f48d3a..c011022eb7 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 a057a73a65..bd3f194e4b 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 2f25c0006e..7e786dfc6c 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 c1154a8c89..322ab0e6d3 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 46c24d253e..caa5471b96 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 773b7b390b..c4406009da 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 748a62bd88..4797bdd92f 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.69"
+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"
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index fb602ee2af..eb1a55be0e 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 7c282b13d8..688db47288 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 8287e2d19d..e16985a1d2 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 0000000000..0dee04408a
--- /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 0000000000..e4b20fd42b
--- /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 0000000000..5632962582
--- /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 4cc35218a0..0000000000
--- 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 c5b9778669..0000000000
--- 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 eb05eb18e0..2fb4f8461f 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 0000000000..2f34d480e0
--- /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 0000000000..80e613e38e
--- /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 0000000000..3d56ebcea1
--- /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 8c34905836..b0ad28fdcf 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 0000000000..ce412f0d43
--- /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 a78cc81c24..a041952fc3 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 0000000000..15dc6aa05e
--- /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 0000000000..7497f8ee45
--- /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 89ef9ee412..0000000000
--- 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 0000000000..e89bfe7a99
--- /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 0000000000..5b9f732ceb
--- /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 0000000000..36d7acabfc
--- /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 0000000000..d12f7b0724
--- /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 0000000000..cf9787e9d3
--- /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 0000000000..3486d169a2
--- /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 0000000000..6498b30d88
--- /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 0000000000..578211b754
--- /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 0000000000..3b337d1e29
--- /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 0000000000..896610a602
--- /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 0000000000..f263536767
--- /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 0000000000..33778d84f1
--- /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 0000000000..8b0dab8cff
--- /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 0000000000..8dd0c61d9f
--- /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 0000000000..b7bf9d818e
--- /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 5c8de4462b..0000000000
--- 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 0000000000..dd1572fde6
--- /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 79e0eda10f..c6bdbec73f 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 0000000000..722e43f2c9
--- /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 529d884809..0000000000
--- 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 7edcf321cb..0000000000
--- 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 5b22b534f4..b0497620aa 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 49a3e93e87..f3c9fb317a 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 e7fc1c5ce4..4078399830 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 28d7f84598..3822401b37 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 a55953010c..0000000000
--- 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 6f4b91e8dc..0000000000
--- 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 879e21c466..bfa63c86c3 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 02e1cebef0..1b7709dcb4 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 7d21ed1138..4668d6db7d 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 b1bc3648d7..01f43588a9 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 989c301e92..8dbd78fab3 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 6105a59c38..825ff85e96 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 0000000000..2ba4893ec8
--- /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 0000000000..b597fcf781
--- /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 0000000000..e7a0488245
--- /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 682596a59d..ba81a5780c 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 d47e61099c..e3a6a64d46 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 00f7a9a17c..29f8997ace 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 51427c6cba..ab39f49bef 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 0ce1d2362b..d5af4ff67f 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 56310a7b58..aed1d7a055 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,9 @@ 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
}
@@ -39,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 cf48d68c70..c3a920dc69 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 607c316b25..029f6ae508 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 c3057298fa..609b6481c2 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 0000000000..86f37c5f20
--- /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 0000000000..f5c65c7283
--- /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 0000000000..9e12866c9c
--- /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 88458d56bb..41ef1d79a2 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 200f1289b4..c8e8b77b32 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 6e079ffe3f..3d13792eeb 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,8 @@ 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 841194abb4..befdf8903b 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 994c9a339c..deefe1a189 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 81a1667f4e..810adbfb3a 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 0000000000..775934716f
--- /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 c22e7adb79..a1af968c34 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 d2fa714880..f277b26bae 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 abe7e17bba..d057e4ecc3 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 9974e36746..bea207eb8a 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 5aae1a3258..c05c24e7de 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 39e13cb619..cd612f5ec1 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 0000000000..416feae8b8
--- /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 81ae3e6b89..59bd1fa773 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 c013ddd587..45fc226cd6 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 16242badb7..5262d65f3d 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 0000000000..d981fa7796
--- /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 0000000000..a26bb18915
--- /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 5c317b1d6a..7f2e823b1e 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 fb5ee5dece..598d799723 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 5ebc252343..395b57df38 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 0000000000..5d4fd8b297
--- /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 86d7bca722..19ac8718d5 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 0000000000..c55e3c2295
--- /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 0000000000..b8e0075504
--- /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 0000000000..74d47797a2
--- /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 0000000000..25cab1429f
--- /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 0000000000..b039cef4e8
--- /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 0000000000..e4199e6d5e
--- /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 0000000000..7ae729309a
--- /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 0000000000..0c4e3cfebc
--- /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 0000000000..adedd83599
--- /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 0000000000..51ae794175
--- /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 0000000000..8876917bf6
--- /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 0000000000..6053695029
--- /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 0000000000..05b6937c15
--- /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 0000000000..dfaa39ae10
--- /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 0000000000..79fcb8fd99
--- /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 0000000000..df0976b468
--- /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 0000000000..bf453c33e0
--- /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 0000000000..8138d4c7f7
--- /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 0000000000..7db70901d3
--- /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 0000000000..28b79194c7
--- /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 0000000000..fd621adbfb
--- /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 0000000000..caee363d51
--- /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 0000000000..d3511fcaed
--- /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 0000000000..6fc85336a8
--- /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 0000000000..f0c44a382c
--- /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 0000000000..5ad2234900
--- /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 0000000000..f92a29c1ae
--- /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 0000000000..5d4ebe0b94
--- /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 0000000000..32169751f0
--- /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 0000000000..f5374cbbc2
--- /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 0000000000..a422fc715b
--- /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 0000000000..d9323e5979
--- /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 0000000000..1cc223b347
--- /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 0000000000..8056c094e8
--- /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 0000000000..0837bd73b2
--- /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 0000000000..ab322427ee
--- /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 0000000000..9f5a6593ed
--- /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 8b5163cf6c..ceed35121a 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 5d0a2993df..1c56c291b4 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 0000000000..05c96cc8af
--- /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 0000000000..87f9bc3735
--- /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 0000000000..d49559e1df
--- /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 980f9eba89..08d906dd98 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 0000000000..2de6d62065
--- /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 0000000000..0baf3d7e9d
--- /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 c517637576..0626c78f28 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 c4e4b913a7..41f6225ee9 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 78059bd4eb..1528c619c3 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 ac2714584c..6d9a31a816 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 0000000000..07fa0ec15d
--- /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 83c7c1aca7..9a9af5ee63 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 068fb02b0f..a907934724 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 94d6653241..3e0deaf9b3 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 6c7a9fb704..863c996b9d 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 3a468eb0f5..4a15f95cea 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
@@ -68,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
@@ -116,12 +119,14 @@ fun MediaViewerView(
) {
MediaViewerTopBar(
actionsEnabled = state.downloadedMedia is AsyncData.Success,
- canDownload = state.canDownload,
- canShare = state.canShare,
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(
@@ -133,6 +138,40 @@ fun MediaViewerView(
}
}
}
+ 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
@@ -278,12 +317,12 @@ private fun rememberShowProgress(downloadedMedia: AsyncData): Boolea
@Composable
private fun MediaViewerTopBar(
actionsEnabled: Boolean,
- canDownload: Boolean,
- canShare: Boolean,
mimeType: String,
senderName: String?,
dateSent: String?,
+ canShowInfo: Boolean,
onBackClick: () -> Unit,
+ onInfoClick: () -> Unit,
eventSink: (MediaViewerEvents) -> Unit,
) {
TopAppBar(
@@ -297,11 +336,15 @@ private fun MediaViewerTopBar(
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
)
}
}
@@ -311,19 +354,6 @@ private fun MediaViewerTopBar(
),
navigationIcon = { BackButton(onClick = onBackClick) },
actions = {
- if (canShare) {
- IconButton(
- enabled = actionsEnabled,
- onClick = {
- eventSink(MediaViewerEvents.Share)
- },
- ) {
- Icon(
- imageVector = CompoundIcons.ShareAndroid(),
- contentDescription = stringResource(id = CommonStrings.action_share)
- )
- }
- }
IconButton(
enabled = actionsEnabled,
onClick = {
@@ -341,20 +371,17 @@ private fun MediaViewerTopBar(
)
}
}
- if (canDownload) {
+ if (canShowInfo) {
IconButton(
+ onClick = onInfoClick,
enabled = actionsEnabled,
- onClick = {
- eventSink(MediaViewerEvents.SaveOnDisk)
- },
) {
Icon(
- imageVector = CompoundIcons.Download(),
- contentDescription = stringResource(id = CommonStrings.action_save),
+ imageVector = CompoundIcons.Info(),
+ contentDescription = null,
)
}
}
- // TODO Add action to open infos.
}
)
}
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 0000000000..a987f8957c
--- /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 0000000000..813d358ec0
--- /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 0000000000..8452eb9158
--- /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 0000000000..85285375e1
--- /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 0000000000..43cee7b95f
--- /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 0000000000..aeaec0a9b0
--- /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 0000000000..087ab1b4c4
--- /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 0000000000..237209e0d7
--- /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 0000000000..cc0f2e573f
--- /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 0000000000..6072c56db8
--- /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 0000000000..cc652f21ec
--- /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 0000000000..2a19be26f3
--- /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 0000000000..c03ebc37c4
--- /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 0000000000..6633fcbce1
--- /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 0000000000..8eeaef976c
--- /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 0000000000..934ed860af
--- /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 c341f6e751..11d8426c09 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 0000000000..c07c53f8ae
--- /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 cbe334216c..1e5f6120cc 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 acbfb57619..83fada0e54 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 a0f36c6f0f..f5f28007d4 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 eceefa879c..eb386ed459 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/res/values-fi/translations.xml b/libraries/textcomposer/impl/src/main/res/values-fi/translations.xml
index 4e5661dce0..a7d2913937 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 4db0f2cb95..04e45678eb 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 1e98029dda..fbea21c512 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 652031d2c2..babe41a142 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 826730030d..0d7acd7e90 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 39b8bc5b9a..5ee3c84a84 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 3871a58914..7baf58a5be 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 cadcd7c6fa..b41246e3b7 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 bb6696ebfa..34643e42d4 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 24bd75cf46..3d75daa029 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 d6e41bc748..4fe2418fa6 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 3755164173..5cf002c334 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 529fc66c06..68547a7f7e 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 71f173b9bf..b9a672e706 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 6373c2fec6..3d84d351a8 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 437d6b63e6..967647b836 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 35e9388ae7..2b18c44044 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 64646a6245..1c26d0eaae 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 8d3b71030a..5fe04394f8 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 5c86a27959..15506c139c 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 80e69850ff..0451985bb2 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 8f4aa8b20c..1c68bef70a 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 0000000000..5beb8ebbc0
--- /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 d124e57dcc..4ea61b8547 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 ff3c5542f6..c35ec0b14c 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 0000000000..1e5c706b10
--- /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 a7d0c15c13..5200614d57 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 75d00240a2..a06181a4ee 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 0000000000..155190e3bb
--- /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 0000000000..48807f5027
--- /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 f1d8e5f987..71357a2559 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 aa339e3365..308edd0a51 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 0000000000..0786d2d7ed
--- /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 fcf1998097..4c7b176fa5 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 9a82b46776..fdc9b2ee50 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 8d2f5b88ac..8867af8287 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 ceedf0948f..59b1891962 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 2730a94337..fe63c2c68e 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 f54cdb81ca..4d24ffebfd 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 0000000000..037c8ce9a1
--- /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 0000000000..3b0faba911
--- /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 0000000000..34c04cbfd4
--- /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 0000000000..5c01877a84
--- /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 0000000000..5688c7d06a
--- /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 0000000000..037c8ce9a1
--- /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 0000000000..037c8ce9a1
--- /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 0000000000..12c9aaa2d6
--- /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 0000000000..134e9bdab9
--- /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 0000000000..cf7480bdac
--- /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 0000000000..5dc14a3d58
--- /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 0000000000..06b28f4d82
--- /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 0000000000..d22b967291
--- /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 0000000000..2db28246d3
--- /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 0000000000..2cf7dca91f
--- /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 0000000000..0af2113d9c
--- /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 0000000000..df489c8ea1
--- /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 0000000000..d97d375fea
--- /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 b0bf21949f..7e30e444ff 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 10051a39ee..cfe31bc8fe 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 fff94dc871..4cb71805e1 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 7f7a3a82b8..fd49a2148d 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 6ff70baa53..b4e80404ff 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 a63acd006b..8f225b0f65 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 ec2c3e8463..26be22d138 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 8fbc79fcf3..fb77e99ad9 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 b56d48d004..614d3a058d 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 8d10d0b0fb..36778ac3b3 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 0fda154e00..ea02c9e4ca 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 5fa9d54233..8988c0e9e2 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 157f8f6272..c1c8843eec 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 4202f8ccbf..0dba7fd3a3 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 d5f291ce6a..99a2236795 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 0bf11f0975..0000000000
--- 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 732ac86d77..8988c0e9e2 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 f5f28f06a1..0000000000
--- 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 3caf8ac57f..435710b521 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 60ae5aae0c..4c366e8e5e 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 0000000000..225fbedca2
--- /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 0000000000..1793f2cd28
--- /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 0000000000..60ae5aae0c
--- /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 c577e8c111..0362d1fd5a 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 55bd273fb7..b3c0d8a1a9 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 f0e7376bf1..144d57de77 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 b62aa5fe3e..e741b95858 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 714c056792..d40b5db1ad 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 6ee127e60f..a430ccafa8 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 28d2d44b52..66ed2c1d1c 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 c19d6aca20..8805b4d219 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 4a5a3a5696..e6d07e3ba6 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 b3b07021b1..e2b84ff5cb 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 427e5ca89f..735b7b6834 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 f633bfa019..441a167459 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 473d54b929..b77eaee61d 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 45cc609343..42ac955c15 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 9dc1afd109..d94ddc8e5b 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 d8024b6d6b..8871186de7 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 c7f71897e3..b3545619f2 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 416513ad1d..5dedf20373 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 620f6be4cd..c9950f17fb 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 741fa7213e..4759f565fa 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 208bdd75f0..6e76284a39 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 7ba1f6f69a..9fe459ab48 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 93378213f2..8c705242a4 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 aa2e03e152..9ff5b22ea1 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 7304223f0b..9fa130f77c 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 770f470eca..b6629b1245 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 7e512a91dd..5ba62eefa6 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 8679141165..9cb36c5613 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 748fb71ce6..c97daa7de0 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 4b10e12a57..34aa4b52aa 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 63928bced5..2a00f84266 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 96e46836ac..d5a95d0d2f 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 e0a94ffc9e..d042837947 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 30905e6234..6068dbf315 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 72918f3603..8b4b53ce1c 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 0dfe364e10..9f5b039858 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 8642af7f91..37f890e3a9 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 ab024d6a8a..f2e0cbba03 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 6f64e91347..49d25686a4 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 c74aa90e56..86b61a41d8 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 7120d62d2b..90a268259d 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 f531ecc171..43c1408518 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 1291a4b1f4..2a007ada50 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 0000000000..acd5520e3f
--- /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 0000000000..eff9f40e2c
--- /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 0000000000..57fdb84910
--- /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 0000000000..c22f29397c
--- /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 0000000000..d9940453cb
--- /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 e5a2758a48..0000000000
--- 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 0000000000..82873fa1af
--- /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 0000000000..58e74b79a2
--- /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 0000000000..70de94855c
--- /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 0000000000..f85206c5a4
--- /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 0000000000..70de94855c
--- /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 0000000000..78189ef5a7
--- /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 0000000000..2e4cf5001b
--- /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 0000000000..10b88c4bb9
--- /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 0000000000..10b88c4bb9
--- /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 0000000000..ae2864d0d4
--- /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 0000000000..3baca8477d
--- /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 0000000000..14bf073dd5
--- /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 0000000000..7f5b267ac5
--- /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 0000000000..3d3b373fdd
--- /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 0000000000..3c2bb54f73
--- /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 0000000000..7453125930
--- /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 0000000000..034b838c95
--- /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 d341017e17..630a153541 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 eb9d617feb..a7adc543f6 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 16aabac42d..aa645ae51b 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 aaf06e57ab..4217c75857 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 d341017e17..630a153541 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 5c10dd38bc..972c016e3e 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 51f5f5770a..9081a2f732 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 7b5d6c9bcd..65b9db12be 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 ef37f98bd8..b6c1f2e50e 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 0456b59845..a53d365ccd 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 ef07526a30..4b664b0055 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 ee40a061ca..16864c1179 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 a3179a0f59..86b1424d2d 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 0ed418ba83..72f44ed719 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 a54b4c686d..d111477810 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 64da7bda0d..58bd7612b5 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 0c1e0f279b..d55b3812f3 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 855c2cb40d..76a546a60c 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 fc57718382..491ce0d9f0 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 ca26a3463f..f32931e67e 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 8d26082157..1235223337 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 ce9698ab3e..7d9fa12efc 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 0000000000..fa60e497cd
--- /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 0000000000..6866b6afc0
--- /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 0000000000..9298210927
--- /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 0000000000..a5e66c073f
--- /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 0000000000..9dda5c0f62
--- /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 0000000000..8eef415284
--- /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 0000000000..462b3bc35e
--- /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 0000000000..ab44725d4c
--- /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 0000000000..29ea2f7789
--- /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 0000000000..43684248f2
--- /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 0000000000..31c1d7e220
--- /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 0000000000..c34f208e49
--- /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 0000000000..02fd5af4b3
--- /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 0000000000..6abdd712c0
--- /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 0000000000..de9b3caf77
--- /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 0000000000..8b36ceefb5
--- /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 0000000000..8f0414d4b3
--- /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 0000000000..2548547d60
--- /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 0000000000..b75df7ecea
--- /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 0000000000..92f98168bb
--- /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 0000000000..f125ca034e
--- /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 0000000000..9463263a2f
--- /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 0000000000..a8e76f0ab7
--- /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 0000000000..c4e4262ce2
--- /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 0000000000..2952c5bd81
--- /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 0000000000..9c47af83a4
--- /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 0000000000..5ea469c53f
--- /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 0000000000..54a5fc7d9c
--- /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 0000000000..613fccf45c
--- /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 0000000000..b9982135b9
--- /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 0000000000..2c6855f6ab
--- /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 0000000000..0bca790524
--- /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 0000000000..6fec3fbf9a
--- /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 0000000000..d626612346
--- /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 0000000000..3cbbf256ec
--- /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 0000000000..215cef276c
--- /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 0000000000..171b5f5aa5
--- /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 a66e7fcd95..a967566289 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 0000000000..683799c494
--- /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 9f5c57aaea..ad9bc11f60 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 0000000000..a47858aeb1
--- /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 0e0342e192..3bf8f0bd3d 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 f500ff60dd..da9d93301b 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 c80b8639e0..e0975da1a9 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 4088be4e1e..6b36ec5059 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 b3dbf77d1d..fe9b8d0f55 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:6b4e4a075bcb4b95455c27ce2e5f9f1bdac4a1aca22ad944dd53fdbaeb6ca970
-size 44550
+oid sha256:22aabc7669365a40151e7ecd82a3324a53d0f55a370e0dd803d843a5275b1434
+size 45949
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 87c07faab3..2f90f04914 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 25da7b8bf1..70d0754dab 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 2bf210d590..8e3e0660c1 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 6de43e00ee..d1cba8905f 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 cf03f3c12f..cf5c641ec7 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 63da19cb01..9778b3513b 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 7e43e760b2..c281247aeb 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 1667fe5ff0..c275736aa0 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 e44f324015..17714bdf54 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 5797dc5345..674ba67cce 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 9b837817e6..faee81cc79 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:7ee53243923d7b5adcc2094d3fe6bb87be00a179cae11966dcb230f2c3e8246e
-size 43681
+oid sha256:6721b3503beb8ddfa9efc52a7ab269c6aea9fcefad2c1b1b9ce31b4240d48719
+size 45204
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 6d0c2a905b..2e828a0005 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 c479ebef74..084646ff71 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 885d6afaed..133a8cbd14 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 13b4a76016..a6e70d1e6e 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 18e022f292..5d6104964e 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 99c6f765a5..720075f007 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.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 8f51f4d170..7fec52751b 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 cff8f7e35f..8f51f4d170 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 afa1dd9b61..cff8f7e35f 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 0000000000..afa1dd9b61
--- /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 8561e18151..3ab5d1cac8 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 a851ce84e5..8561e18151 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 212726df37..a851ce84e5 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 0000000000..212726df37
--- /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.preferences.impl.root_PreferencesRootViewDark_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.root_PreferencesRootViewDark_0_en.png
index faf2367a0b..c13d3cb3ba 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:48320aed4570138a76b04b08f37f67098a72e1b63f13273fd6d9c0a6e33b7e10
-size 37955
+oid sha256:4a6a54efb3bb7eeb97cfa06995a199eb83a7e630fb1b631d7992b464e0b04d20
+size 37954
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 5ba26e89ea..ef6e64aa89 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:6a71b634518191f299c924f8ddd39b44ddc1255698e981796bb8b034377c515f
-size 37712
+oid sha256:d35962150a8dc5b7d774af160a994182afbe2d5538e204b2545c02287897cd99
+size 37711
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 b64b5e290d..4e534c9041 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:e33a80d4f6dc4a1bd1cd86b2bfd64471926871f92d8d83cae6b32a79459c8ea0
-size 38775
+oid sha256:a3711fb587d635b28a398bb5b475bee16d4abdbf691d494348fe11e270e0624d
+size 38774
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 344a42b7a5..8d27190ccb 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:c69e1d5748e65b35525df64b83c6616ab439299284ab2bda3a8408d5f4139d2f
-size 38802
+oid sha256:41b91c8a1805bb24453c6f85091704e609a01f83a86ca17979d97de4c20e4cbe
+size 38800
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 76446e6180..12b3132118 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 52912aa0fd..58d7c45b3d 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 0ae9c7dc0e..e2e7745abc 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 fba01a25c6..09080780f9 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 b3273a0efd..62a3a99fba 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 4a0208786f..4f70fd407b 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 16b2995961..5591d4b251 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 cedbb0f72d..406387611a 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 f7c16b996f..ac33cc9543 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 1774b7ba41..15c85d775e 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:11c9af054eb293134755003d8864305d9f2ef9a7597df795e4354e4f7c420166
-size 42209
+oid sha256:9f50132f14d4f26077cbb44e616b4b76070fd6df20e14af740e2743d1fc874b8
+size 40020
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 8b0e9ee674..55ca265ccf 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:d3bd2e90f06f259b158b6438ff4cd045733362ad215b933ddbb855043a5b8fa2
-size 38463
+oid sha256:76f09daad900e1c3eac91d07ec81c6a714f80ae9a734e3a3921dc5b259bab278
+size 40502
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 52789eb061..4bc9febcfd 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 aa849d237d..4aaa0ce01a 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 c4aca87a18..3a7de20375 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 a6694758f1..8e8c5d5fcd 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 c09b745e24..11c6879648 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 c3ddd6dd92..2485aea5d3 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 ea86ba1b2e..c6b95729a7 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 b4d73c57b7..72c9a2497c 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 fec1bbe806..34156265b9 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 7f4fb4df94..17e997b919 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 fe253a9a6e..cdf150fbc0 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 d0bb2ef17c..5c7a431cbd 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 045e6fbe0a..686029b8da 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:7cf9b0b10c112a964bbc85e6c14b634d96460884992c53e8ae9408ec5a94455a
-size 43073
+oid sha256:1d133a0d749da6a68605594ce69494fb8bb8094023a80e617872a40789bd4213
+size 40891
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 8a5a3a51ae..48df048115 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:1aa79cc35f4f4e9f6221b1f6cf119906bef17cc62268fae01ff1ec713931b7b9
-size 39619
+oid sha256:fbecca870bf0bedc0db8195b222b75408e8a2846424cf9de9fcf86e7342734d9
+size 41750
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 8656744f17..12816f97f3 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 34883fce29..3e61505a0e 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 5fa82cc6ce..d4bea56772 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 9939b2f5fc..7d28a0e898 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 64b1a49349..0f84095ec7 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 e3c391e6ba..5206ffd141 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 e3c391e6ba..5206ffd141 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 1ea727091e..f3c7ad9543 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 64b1a49349..0f84095ec7 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 e3c391e6ba..5206ffd141 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 e3c391e6ba..5206ffd141 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 ec9abfb944..b37ea9738e 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 7eaaa4292b..9a235d85c5 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 ad35d903c9..5eab82d590 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 ad35d903c9..5eab82d590 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 2c27fddc9a..b248cd5960 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 7eaaa4292b..9a235d85c5 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 ad35d903c9..5eab82d590 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 ad35d903c9..5eab82d590 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 5f5470c52c..ec2a773363 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 15916a2f9c..7e94c40141 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 c8d4132209..7cbb9c3fbe 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 c8d4132209..7cbb9c3fbe 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 cb557befe2..df1b3a3548 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 6dc2165b3d..01d621ac7f 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 a62217d12e..e69ddadd5a 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 ff6109ec98..cceb41304b 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 ff6109ec98..cceb41304b 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 3c9ab417db..ae0e7d8078 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 bd4be5fcb3..3b466b3aa0 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 0cb67f883f..5d0f2b097c 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 c8d4132209..7cbb9c3fbe 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 c8d4132209..7cbb9c3fbe 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 cb557befe2..df1b3a3548 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 4c5e8940c8..cd6dda7370 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 af5a72d802..d6a004275e 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 ff6109ec98..cceb41304b 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 ff6109ec98..cceb41304b 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 3c9ab417db..ae0e7d8078 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/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 0000000000..7c2a059e7c
--- /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 0000000000..160c9d66fd
--- /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 0000000000..36a6b1bf7a
--- /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 0000000000..ef7b68df39
--- /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 0000000000..7b48600104
--- /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 0000000000..cf85f766ab
--- /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 0000000000..b27ebae0ce
--- /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 0000000000..1698660c8e
--- /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 0000000000..3c5b96d217
--- /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 0000000000..1100737467
--- /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 0000000000..72c687d439
--- /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 0000000000..d78e8bad2a
--- /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 0000000000..deb69c6aa1
--- /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 0000000000..bd51d8c202
--- /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 3c573bace1..18fe30dd7a 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 2acb4b3d25..0ef108d30b 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 261e330a8f..0d92b4ca6d 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 c057b5283e..36f2db136e 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 d51d509823..951f776d62 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 b7c3ab2d68..d51d509823 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 0000000000..b7c3ab2d68
--- /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 316ef83354..5a1526f50b 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 0f1ab6cc74..316ef83354 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 0000000000..0f1ab6cc74
--- /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 2b971f938f..ca94a884e3 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 af0fbca712..6793e1a405 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 0000000000..137a7d8afe
--- /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 0000000000..7a2ce52d92
--- /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 0000000000..be45c32ec9
--- /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 0000000000..440c3309cf
--- /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 0000000000..02a746e1d8
--- /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 0000000000..9ccb8d78d2
--- /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 0000000000..aea3483115
--- /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 0000000000..d79c6a8d25
--- /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 0000000000..04df2c1c11
--- /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 0000000000..11c187832f
--- /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 0000000000..fd7b7e6443
--- /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 0000000000..f835ed50a3
--- /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 0000000000..a0e5cbaec3
--- /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 0000000000..8430ef926c
--- /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 0000000000..56d561434c
--- /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 0000000000..7c0ae7e7de
--- /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 0000000000..60abaf54cf
--- /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 0000000000..412d151f90
--- /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 0000000000..89ea4bcb8b
--- /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 0000000000..31cbcbc59b
--- /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 0000000000..a027c89303
--- /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 0000000000..503f2bb229
--- /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 0000000000..f5cb7d82cd
--- /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 0000000000..6665c107c1
--- /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 0000000000..d58f4c3155
--- /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 0000000000..dc363be75b
--- /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 0000000000..1c0a01c77f
--- /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 0000000000..f0839c5104
--- /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 0000000000..859c045a16
--- /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 0000000000..2e0831e58a
--- /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 0000000000..4387dbac9d
--- /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 0000000000..c69aff8a3f
--- /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 0000000000..2acff7a04e
--- /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 0000000000..01146031c3
--- /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 0000000000..38b1cfb67e
--- /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 0000000000..a1e77b2b01
--- /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 0000000000..7234dad634
--- /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 0000000000..6e460fc429
--- /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 0000000000..d9e5fffc64
--- /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 0000000000..872a6e3520
--- /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 0000000000..5d2152939f
--- /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 0000000000..310557b2bd
--- /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 0000000000..ea2f0891af
--- /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 0000000000..677aaf260b
--- /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 0000000000..b17c686b18
--- /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 0000000000..9cec961966
--- /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 0000000000..f7d527bf6d
--- /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 0000000000..0d080b8cd0
--- /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 0000000000..9807f8bd54
--- /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 0000000000..1f1b966fff
--- /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 0000000000..1f1b966fff
--- /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 0000000000..07edef2141
--- /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 0000000000..ca39d4ccea
--- /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 0000000000..cc888a4d57
--- /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 0000000000..5d694d039b
--- /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 0000000000..8baa855f7b
--- /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 0000000000..6ead8ed866
--- /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 0000000000..fa9f362ab4
--- /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 0000000000..8844a9f4a3
--- /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 0000000000..5c067f246f
--- /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 0000000000..73e39f4bd6
--- /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 0000000000..73e39f4bd6
--- /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 0000000000..700079a0db
--- /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 0000000000..2149a46ba7
--- /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 0000000000..158c181240
--- /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 0000000000..4ae1e1c6cf
--- /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 0000000000..70d447adcd
--- /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 0000000000..41b0cc2f9a
--- /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 0000000000..876fab066d
--- /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 0000000000..50727c043f
--- /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 c4d024965e..0000000000
--- 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 37f1c2ed22..0000000000
--- 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 0000000000..b5b75d1b63
--- /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 0000000000..336b57074a
--- /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 93393fba05..fd5c2af6e6 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:f8672f053256e9ab2984aa06a22e4c27f2dd3f3373f3d87c12bc3269297713fd
-size 389602
+oid sha256:66437179fb0b851d4d4d647d00cab94cc7422d625f559839c675b378dbf1af38
+size 389408
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 0000000000..4a43ce31da
--- /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 0000000000..f3d0c19a9f
--- /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 0000000000..6d8afe1140
--- /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 4439239760..12d5df3fa1 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:33f460febda376d35c6eceb643dca374e9dbc76d30f21ccab99085a628e90e79
-size 389629
+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 061278ca3f..e3b32d36d3 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:1b5c823df53d7a6fb2df235c2016b0e71bbf26aa2f7ec1c175e3360424b69c37
-size 94822
+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 5d77b5705d..40ff36cd94 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:4b5506ab9651470ae3fe08d89a1ee2783d333a0fc1a657e45d50c156d664e84c
-size 396617
+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 dcbe73cf41..d94989c757 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:e4a44ab880d3d9660c6671505d0575f6a89e21c4e4a16d5838b6826fe770473d
-size 22041
+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 c8856654b4..3603786361 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:1c7122bd07a25e38d8056ca2f9d297e2073c418d359b4d286a5ba3441bce1e4b
-size 5712
+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 fbda655334..6b6e6a655d 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:f67a1db4ce45d9b5d2cf8f9f3bf0e5e6de439d1d9631a407826f984afdaf90c7
-size 14756
+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 4ff875eb5c..32e7fcef89 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:3b9d8de8cddf75a662c72d519eef8ae6409e109d944826147f71ddc97f2d5a40
-size 14954
+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 1d9b20bcf3..dc33c0aef4 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:7e202c4748e6c0afcfdcfb77c3e3f14a234d0622e579ea926311e669486ba0e8
-size 13576
+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 9698527a7d..d115aaa7ed 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:646a010fc57669e8bd7a97cab1f5bfdd94c78a05cd02c0150a6b41ef82e8f466
-size 13687
+oid sha256:035ef0079af6e9825a52b86e2eab50667404a66d70dd2756596b60cc1cea376a
+size 26404
diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml
index 1e73ef425d..0ebe19bcc4 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 f18744bae9..2efd8eac97 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 f0c0378333..7c65604167 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